Merge branch 'master' into analyzer-def-using-search-mapping

This commit is contained in:
James Agnew 2017-11-13 13:32:40 -05:00 committed by GitHub
commit 49f4ac4fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1391 changed files with 255764 additions and 37095 deletions

2
.gitignore vendored
View File

@ -141,6 +141,8 @@ local.properties
**/.target **/.target
**/.project **/.project
**/.classpath **/.classpath
**/.factorypath
**/.springBeans
# PDT-specific # PDT-specific

View File

@ -22,4 +22,4 @@ before_script:
script: script:
# - mvn -e -B clean install && cd hapi-fhir-ra && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID clean test jacoco:report coveralls:report # - mvn -e -B clean install && cd hapi-fhir-ra && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID clean test jacoco:report coveralls:report
# - mvn -Dci=true -e -B -P ALLMODULES,NOPARALLEL,ERRORPRONE clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report # - mvn -Dci=true -e -B -P ALLMODULES,NOPARALLEL,ERRORPRONE clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report
- mvn -Dci=true -e -B -P ALLMODULES,MINPARALLEL clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report - mvn -Dci=true -e -B -P ALLMODULES,MINPARALLEL,ERRORPRONE clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report

View File

@ -63,6 +63,65 @@
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
<plugin>
<groupId>org.basepom.maven</groupId>
<artifactId>duplicate-finder-maven-plugin</artifactId>
<executions>
<execution>
<id>default</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<inherited>true</inherited>
</execution>
</executions>
<configuration>
<printEqualFiles>false</printEqualFiles>
<failBuildInCaseOfDifferentContentConflict>true</failBuildInCaseOfDifferentContentConflict>
<failBuildInCaseOfEqualContentConflict>true</failBuildInCaseOfEqualContentConflict>
<failBuildInCaseOfConflict>true</failBuildInCaseOfConflict>
<checkCompileClasspath>true</checkCompileClasspath>
<checkRuntimeClasspath>false</checkRuntimeClasspath>
<checkTestClasspath>false</checkTestClasspath>
<skip>false</skip>
<quiet>false</quiet>
<preferLocal>true</preferLocal>
<useResultFile>true</useResultFile>
<resultFileMinClasspathCount>2</resultFileMinClasspathCount>
<resultFile>${project.build.directory}/duplicate-finder-result.xml</resultFile>
<ignoredDependencies>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
</dependency>
<dependency>
<groupId>com.phloc</groupId>
<artifactId>phloc-commons</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>
<dependency>
<groupId>org.jscience</groupId>
<artifactId>jscience</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
</dependency>
</ignoredDependencies>
<ignoredResources>
<ignoredResource>changelog.txt</ignoredResource>
<ignoredResource>javac.bat</ignoredResource>
</ignoredResources>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>

View File

@ -35,6 +35,25 @@
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
</exclusion> </exclusion>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -92,52 +111,14 @@
</configuration> </configuration>
<executions> <executions>
<execution> <execution>
<id>dstu2_shade</id> <id>it</id>
<goals> <goals>
<goal>integration-test</goal> <goal>integration-test</goal>
<goal>verify</goal>
</goals> </goals>
<configuration> <configuration>
<includes> <includes>
<include>**/*Dstu2ShadeIT.java</include> <include>**/*IT.java</include>
</includes> </includes>
<classpathDependencyExcludes>
<!--<classpathDependencyExclude>ca.uhn.hapi.fhir:*</classpathDependencyExclude>-->
<classpathDependencyExclude>org.codehaus.woodstox:woodstox-core-asl</classpathDependencyExclude>
<classpathDependencyExclude>org.codehaus.woodstox:stax2-api</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</execution>
<execution>
<id>dstu2</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<includes>
<include>**/*Dstu2IT.java</include>
</includes>
<classpathDependencyExcludes>
<classpathDependencyExclude>org.codehaus.woodstox:woodstox-core-asl</classpathDependencyExclude>
<classpathDependencyExclude>org.codehaus.woodstox:stax2-api</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</execution>
<execution>
<id>dstu3</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<includes>
<include>**/*Dstu3IT.java</include>
</includes>
<classpathDependencyExcludes>
<classpathDependencyExclude>org.codehaus.woodstox:woodstox-core-asl</classpathDependencyExclude>
<classpathDependencyExclude>org.codehaus.woodstox:stax2-api</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -9,6 +9,7 @@ import java.util.zip.ZipFile;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -21,6 +22,7 @@ public class BuiltJarDstu2ShadeIT {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BuiltJarDstu2ShadeIT.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BuiltJarDstu2ShadeIT.class);
@Test @Test
@Ignore
public void testParserXml() throws Exception { public void testParserXml() throws Exception {
FhirContext ctx = FhirContext.forDstu2(); FhirContext ctx = FhirContext.forDstu2();

View File

@ -7,7 +7,6 @@ import static org.mockito.Mockito.when;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
import org.apache.http.client.ClientProtocolException;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.junit.*; import org.junit.*;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@ -82,6 +81,7 @@ public class GenericClientDstu3IT {
* TODO: narratives don't work without stax * TODO: narratives don't work without stax
*/ */
@Test @Test
@Ignore
public void testBinaryCreateWithFhirContentType() throws Exception { public void testBinaryCreateWithFhirContentType() throws Exception {
IParser p = ourCtx.newXmlParser(); IParser p = ourCtx.newXmlParser();
@ -142,7 +142,7 @@ public class GenericClientDstu3IT {
.returnBundle(Bundle.class) .returnBundle(Bundle.class)
.execute(); .execute();
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(idx).url().toString()); assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(idx).url().toString());
idx++; idx++;
} }
@ -177,12 +177,12 @@ public class GenericClientDstu3IT {
Request request = capt.getAllValues().get(0); Request request = capt.getAllValues().get(0);
ourLog.info(request.headers().toString()); ourLog.info(request.headers().toString());
assertEquals("http://example.com/fhir/Binary", request.url().toString()); assertEquals("http://example.com/fhir/Binary?_format=json", request.url().toString());
validateUserAgent(capt); validateUserAgent(capt);
assertEquals(Constants.CT_FHIR_XML_NEW + ";charset=utf-8", request.body().contentType().toString().toLowerCase().replace(" ", "")); assertEquals(Constants.CT_FHIR_JSON_NEW + ";charset=utf-8", request.body().contentType().toString().toLowerCase().replace(" ", ""));
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, request.header("Accept")); assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, request.header("Accept"));
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent()); assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourCtx.newJsonParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent());
} }
@ -257,11 +257,11 @@ public class GenericClientDstu3IT {
assertNotNull(outcome.getResource()); assertNotNull(outcome.getResource());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">FINAL VALUE</div>", ((Patient) outcome.getResource()).getText().getDivAsString()); assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">FINAL VALUE</div>", ((Patient) outcome.getResource()).getText().getDivAsString());
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).url().toString()); assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(0).url().toString());
} }
private ArgumentCaptor<Request> prepareClientForSearchResponse() throws IOException, ClientProtocolException { private ArgumentCaptor<Request> prepareClientForSearchResponse() throws IOException {
final String respString = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; final String respString = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
myHttpResponse = new Response.Builder() myHttpResponse = new Response.Builder()
.request(myRequest) .request(myRequest)

View File

@ -28,6 +28,7 @@
<dependency> <dependency>
<groupId>org.codehaus.woodstox</groupId> <groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId> <artifactId>woodstox-core-asl</artifactId>
<optional>true</optional>
</dependency> </dependency>
<!-- Only required for narrative generator support --> <!-- Only required for narrative generator support -->
@ -124,6 +125,7 @@
<argLine>${argLine} -Dfile.encoding=UTF-8 -Xmx712m</argLine> <argLine>${argLine} -Dfile.encoding=UTF-8 -Xmx712m</argLine>
</configuration> </configuration>
</plugin> </plugin>
<!--
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-source-plugin</artifactId>
@ -136,6 +138,7 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
-->
</plugins> </plugins>
<resources> <resources>
<resource> <resource>

View File

@ -78,6 +78,10 @@ public enum FhirVersionEnum {
return myVersionImplementation; return myVersionImplementation;
} }
public boolean isEqualOrNewerThan(FhirVersionEnum theVersion) {
return ordinal() >= theVersion.ordinal();
}
public boolean isEquivalentTo(FhirVersionEnum theVersion) { public boolean isEquivalentTo(FhirVersionEnum theVersion) {
if (this.equals(theVersion)) { if (this.equals(theVersion)) {
return true; return true;

View File

@ -152,7 +152,8 @@ public class RuntimeSearchParam {
public enum RuntimeSearchParamStatusEnum { public enum RuntimeSearchParamStatusEnum {
ACTIVE, ACTIVE,
DRAFT, DRAFT,
RETIRED RETIRED,
UNKNOWN
} }
} }

View File

@ -20,29 +20,29 @@ package ca.uhn.fhir.model.primitive;
* #L% * #L%
*/ */
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.model.api.BasePrimitive; import ca.uhn.fhir.model.api.BasePrimitive;
import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.api.annotation.SimpleSetter; import ca.uhn.fhir.model.api.annotation.SimpleSetter;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.util.XmlDetectionUtil;
import ca.uhn.fhir.util.XmlUtil; import ca.uhn.fhir.util.XmlUtil;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* Note that as of HAPI FHIR 3.1.0, this method no longer uses
* the StAX XMLEvent type as the XML representation, and uses a
* String instead. If you need to work with XML as StAX events, you
* can use the {@link XmlUtil#parse(String)} and {@link XmlUtil#encode(List)}
* methods to do so.
*/
@DatatypeDef(name = "xhtml") @DatatypeDef(name = "xhtml")
public class XhtmlDt extends BasePrimitive<List<XMLEvent>> { public class XhtmlDt extends BasePrimitive<String> {
private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\""; private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\"";
private static final String DIV_OPEN_FIRST = "<div" + DECL_XMLNS + ">"; public static final String DIV_OPEN_FIRST = "<div" + DECL_XMLNS + ">";
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
@ -54,7 +54,7 @@ public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
/** /**
* Constructor which accepts a string code * Constructor which accepts a string code
* *
* @see #setValueAsString(String) for a description of how this value is applied * @see #setValueAsString(String) for a description of how this value is applied
*/ */
@SimpleSetter() @SimpleSetter()
@ -63,29 +63,12 @@ public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
} }
@Override @Override
protected String encode(List<XMLEvent> theValue) { protected String encode(String theValue) {
try { return theValue;
StringWriter w = new StringWriter();
XMLEventWriter ew = XmlUtil.createXmlFragmentWriter(w);
for (XMLEvent next : getValue()) {
if (next.isCharacters()) {
ew.add(next);
} else {
ew.add(next);
}
}
ew.close();
return w.toString();
} catch (XMLStreamException e) {
throw new DataFormatException("Problem with the contained XML events", e);
} catch (FactoryConfigurationError e) {
throw new ConfigurationException(e);
}
} }
public boolean hasContent() { public boolean hasContent() {
return getValue() != null && getValue().size() > 0; return isNotBlank(getValue());
} }
@Override @Override
@ -94,40 +77,37 @@ public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
} }
@Override @Override
protected List<XMLEvent> parse(String theValue) { protected String parse(String theValue) {
String val = theValue.trim(); if (XmlDetectionUtil.isStaxPresent()) {
if (!val.startsWith("<")) { // for validation
val = DIV_OPEN_FIRST + val + "</div>"; XmlUtil.parse(theValue);
}
boolean hasProcessingInstruction = val.startsWith("<?");
if (hasProcessingInstruction && val.endsWith("?>")) {
return null;
} }
return theValue;
}
try {
ArrayList<XMLEvent> value = new ArrayList<XMLEvent>();
StringReader reader = new StringReader(val);
XMLEventReader er = XmlUtil.createXmlReader(reader);
boolean first = true;
while (er.hasNext()) {
XMLEvent next = er.nextEvent();
if (first) {
first = false;
continue;
}
if (er.hasNext()) {
// don't add the last event
value.add(next);
}
}
return value;
} catch (XMLStreamException e) { /**
throw new DataFormatException("String does not appear to be valid XML/XHTML (error is \"" + e.getMessage() + "\"): " + theValue, e); * Note that as of HAPI FHIR 3.1.0, this method no longer uses
} catch (FactoryConfigurationError e) { * the StAX XMLEvent type as the XML representation, and uses a
throw new ConfigurationException(e); * String instead. If you need to work with XML as StAX events, you
} * can use the {@link XmlUtil#parse(String)} and {@link XmlUtil#encode(List)}
* methods to do so.
*/
@Override
public String getValue() {
return super.getValue();
}
/**
* Note that as of HAPI FHIR 3.1.0, this method no longer uses
* the StAX XMLEvent type as the XML representation, and uses a
* String instead. If you need to work with XML as StAX events, you
* can use the {@link XmlUtil#parse(String)} and {@link XmlUtil#encode(List)}
* methods to do so.
*/
@Override
public BasePrimitive<String> setValue(String theValue) throws DataFormatException {
return super.setValue(theValue);
} }
/** /**
@ -157,7 +137,7 @@ public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
if (value.charAt(0) != '<') { if (value.charAt(0) != '<') {
value = DIV_OPEN_FIRST + value + "</div>"; value = DIV_OPEN_FIRST + value + "</div>";
} }
boolean hasProcessingInstruction = value.startsWith("<?"); boolean hasProcessingInstruction = value.startsWith("<?");
int firstTagIndex = value.indexOf("<", hasProcessingInstruction ? 1 : 0); int firstTagIndex = value.indexOf("<", hasProcessingInstruction ? 1 : 0);
if (firstTagIndex != -1) { if (firstTagIndex != -1) {

View File

@ -43,7 +43,7 @@ public abstract class BaseParser implements IParser {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class);
private ContainedResources myContainedResources; private ContainedResources myContainedResources;
private boolean myEncodeElementsAppliesToChildResourcesOnly;
private FhirContext myContext; private FhirContext myContext;
private Set<String> myDontEncodeElements; private Set<String> myDontEncodeElements;
private boolean myDontEncodeElementsIncludesStars; private boolean myDontEncodeElementsIncludesStars;
@ -556,6 +556,16 @@ public abstract class BaseParser implements IParser {
&& theIncludedResource == false; && theIncludedResource == false;
} }
@Override
public boolean isEncodeElementsAppliesToChildResourcesOnly() {
return myEncodeElementsAppliesToChildResourcesOnly;
}
@Override
public void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly) {
myEncodeElementsAppliesToChildResourcesOnly = theEncodeElementsAppliesToChildResourcesOnly;
}
@Override @Override
public boolean isOmitResourceId() { public boolean isOmitResourceId() {
return myOmitResourceId; return myOmitResourceId;
@ -1039,7 +1049,13 @@ public abstract class BaseParser implements IParser {
} }
private boolean checkIfParentShouldBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) { private boolean checkIfParentShouldBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) {
return checkIfPathMatchesForEncoding(thePathBuilder, theStarPass, myEncodeElementsAppliesToResourceTypes, myEncodeElements, true); Set<String> encodeElements = myEncodeElements;
if (encodeElements != null && encodeElements.isEmpty() == false) {
if (isEncodeElementsAppliesToChildResourcesOnly() && !mySubResource) {
encodeElements = null;
}
}
return checkIfPathMatchesForEncoding(thePathBuilder, theStarPass, myEncodeElementsAppliesToResourceTypes, encodeElements, true);
} }
private boolean checkIfParentShouldNotBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) { private boolean checkIfParentShouldNotBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) {
@ -1058,6 +1074,9 @@ public abstract class BaseParser implements IParser {
} else { } else {
thePathBuilder.append(myResDef.getName()); thePathBuilder.append(myResDef.getName());
} }
if (theElements == null) {
return true;
}
if (theElements.contains(thePathBuilder.toString())) { if (theElements.contains(thePathBuilder.toString())) {
return true; return true;
} }

View File

@ -206,6 +206,22 @@ public interface IParser {
*/ */
void setEncodeElements(Set<String> theEncodeElements); void setEncodeElements(Set<String> theEncodeElements);
/**
* If set to <code>true</code> (default is false), the values supplied
* to {@link #setEncodeElements(Set)} will not be applied to the root
* resource (typically a Bundle), but will be applied to any sub-resources
* contained within it (i.e. search result resources in that bundle)
*/
void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly);
/**
* If set to <code>true</code> (default is false), the values supplied
* to {@link #setEncodeElements(Set)} will not be applied to the root
* resource (typically a Bundle), but will be applied to any sub-resources
* contained within it (i.e. search result resources in that bundle)
*/
boolean isEncodeElementsAppliesToChildResourcesOnly();
/** /**
* If provided, tells the parse which resource types to apply {@link #setEncodeElements(Set) encode elements} to. Any * If provided, tells the parse which resource types to apply {@link #setEncodeElements(Set) encode elements} to. Any
* resource types not specified here will be encoded completely, with no elements excluded. * resource types not specified here will be encoded completely, with no elements excluded.

View File

@ -19,19 +19,6 @@ package ca.uhn.fhir.parser;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.*;
import java.io.*;
import java.math.BigDecimal;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.text.WordUtils;
import org.hl7.fhir.instance.model.api.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.*;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum; import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
@ -46,9 +33,27 @@ import ca.uhn.fhir.parser.json.*;
import ca.uhn.fhir.parser.json.JsonLikeValue.ScalarType; import ca.uhn.fhir.parser.json.JsonLikeValue.ScalarType;
import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType; import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.ElementUtil; import ca.uhn.fhir.util.ElementUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.text.WordUtils;
import org.hl7.fhir.instance.model.api.*;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.ID_DATATYPE; import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.ID_DATATYPE;
import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE; import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE;
import static org.apache.commons.lang3.StringUtils.*;
/** /**
* This class is the FHIR JSON parser/encoder. Users should not interact with this class directly, but should use * This class is the FHIR JSON parser/encoder. Users should not interact with this class directly, but should use
@ -88,7 +93,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private boolean addToHeldExtensions(int valueIdx, List<? extends IBaseExtension<?, ?>> ext, ArrayList<ArrayList<HeldExtension>> list, boolean theIsModifier, CompositeChildElement theChildElem, private boolean addToHeldExtensions(int valueIdx, List<? extends IBaseExtension<?, ?>> ext, ArrayList<ArrayList<HeldExtension>> list, boolean theIsModifier, CompositeChildElement theChildElem,
CompositeChildElement theParent) { CompositeChildElement theParent) {
if (ext.size() > 0) { if (ext.size() > 0) {
list.ensureCapacity(valueIdx); list.ensureCapacity(valueIdx);
while (list.size() <= valueIdx) { while (list.size() <= valueIdx) {
@ -139,12 +144,6 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
return retVal; return retVal;
} }
@Override
protected void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException {
JsonLikeWriter eventWriter = createJsonWriter(theWriter);
doEncodeResourceToJsonLikeWriter(theResource, eventWriter);
}
public void doEncodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException { public void doEncodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException {
if (myPrettyPrint) { if (myPrettyPrint) {
theEventWriter.setPrettyPrint(myPrettyPrint); theEventWriter.setPrettyPrint(myPrettyPrint);
@ -156,6 +155,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
theEventWriter.flush(); theEventWriter.flush();
} }
@Override
protected void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException {
JsonLikeWriter eventWriter = createJsonWriter(theWriter);
doEncodeResourceToJsonLikeWriter(theResource, eventWriter);
}
@Override @Override
public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) { public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) {
JsonLikeStructure jsonStructure = new GsonStructure(); JsonLikeStructure jsonStructure = new GsonStructure();
@ -191,136 +196,136 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBase theNextValue, private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBase theNextValue,
BaseRuntimeElementDefinition<?> theChildDef, String theChildName, boolean theContainedResource, boolean theSubResource, CompositeChildElement theChildElem, BaseRuntimeElementDefinition<?> theChildDef, String theChildName, boolean theContainedResource, boolean theSubResource, CompositeChildElement theChildElem,
boolean theForceEmpty) throws IOException { boolean theForceEmpty) throws IOException {
switch (theChildDef.getChildType()) { switch (theChildDef.getChildType()) {
case ID_DATATYPE: { case ID_DATATYPE: {
IIdType value = (IIdType) theNextValue; IIdType value = (IIdType) theNextValue;
String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue(); String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue();
if (isBlank(encodedValue)) { if (isBlank(encodedValue)) {
break; break;
} }
if (theChildName != null) { if (theChildName != null) {
write(theEventWriter, theChildName, encodedValue); write(theEventWriter, theChildName, encodedValue);
} else { } else {
theEventWriter.write(encodedValue); theEventWriter.write(encodedValue);
}
break;
}
case PRIMITIVE_DATATYPE: {
final IPrimitiveType<?> value = (IPrimitiveType<?>) theNextValue;
if (isBlank(value.getValueAsString())) {
if (theForceEmpty) {
theEventWriter.writeNull();
} }
break; break;
} }
case PRIMITIVE_DATATYPE: {
if (value instanceof IBaseIntegerDatatype) { final IPrimitiveType<?> value = (IPrimitiveType<?>) theNextValue;
if (theChildName != null) { if (isBlank(value.getValueAsString())) {
write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue()); if (theForceEmpty) {
} else { theEventWriter.writeNull();
theEventWriter.write(((IBaseIntegerDatatype) value).getValue());
}
} else if (value instanceof IBaseDecimalDatatype) {
BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue();
decimalValue = new BigDecimal(decimalValue.toString()) {
private static final long serialVersionUID = 1L;
@Override
public String toString() {
return value.getValueAsString();
} }
}; break;
if (theChildName != null) {
write(theEventWriter, theChildName, decimalValue);
} else {
theEventWriter.write(decimalValue);
} }
} else if (value instanceof IBaseBooleanDatatype) {
if (theChildName != null) { if (value instanceof IBaseIntegerDatatype) {
write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue()); if (theChildName != null) {
write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue());
} else {
theEventWriter.write(((IBaseIntegerDatatype) value).getValue());
}
} else if (value instanceof IBaseDecimalDatatype) {
BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue();
decimalValue = new BigDecimal(decimalValue.toString()) {
private static final long serialVersionUID = 1L;
@Override
public String toString() {
return value.getValueAsString();
}
};
if (theChildName != null) {
write(theEventWriter, theChildName, decimalValue);
} else {
theEventWriter.write(decimalValue);
}
} else if (value instanceof IBaseBooleanDatatype) {
if (theChildName != null) {
write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue());
} else {
Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue();
if (booleanValue != null) {
theEventWriter.write(booleanValue.booleanValue());
}
}
} else { } else {
Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue(); String valueStr = value.getValueAsString();
if (booleanValue != null) { if (theChildName != null) {
theEventWriter.write(booleanValue.booleanValue()); write(theEventWriter, theChildName, valueStr);
} else {
theEventWriter.write(valueStr);
} }
} }
} else { break;
String valueStr = value.getValueAsString(); }
case RESOURCE_BLOCK:
case COMPOSITE_DATATYPE: {
if (theChildName != null) { if (theChildName != null) {
write(theEventWriter, theChildName, valueStr); theEventWriter.beginObject(theChildName);
} else { } else {
theEventWriter.write(valueStr); theEventWriter.beginObject();
} }
encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theChildElem);
theEventWriter.endObject();
break;
} }
break; case CONTAINED_RESOURCE_LIST:
} case CONTAINED_RESOURCES: {
case RESOURCE_BLOCK:
case COMPOSITE_DATATYPE: {
if (theChildName != null) {
theEventWriter.beginObject(theChildName);
} else {
theEventWriter.beginObject();
}
encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theChildElem);
theEventWriter.endObject();
break;
}
case CONTAINED_RESOURCE_LIST:
case CONTAINED_RESOURCES: {
/* /*
* Disabled per #103 ContainedDt value = (ContainedDt) theNextValue; for (IResource next : * Disabled per #103 ContainedDt value = (ContainedDt) theNextValue; for (IResource next :
* value.getContainedResources()) { if (getContainedResources().getResourceId(next) != null) { continue; } * value.getContainedResources()) { if (getContainedResources().getResourceId(next) != null) { continue; }
* encodeResourceToJsonStreamWriter(theResDef, next, theWriter, null, true, * encodeResourceToJsonStreamWriter(theResDef, next, theWriter, null, true,
* fixContainedResourceId(next.getId().getValue())); } * fixContainedResourceId(next.getId().getValue())); }
*/ */
List<IBaseResource> containedResources = getContainedResources().getContainedResources(); List<IBaseResource> containedResources = getContainedResources().getContainedResources();
if (containedResources.size() > 0) { if (containedResources.size() > 0) {
beginArray(theEventWriter, theChildName); beginArray(theEventWriter, theChildName);
for (IBaseResource next : containedResources) { for (IBaseResource next : containedResources) {
IIdType resourceId = getContainedResources().getResourceId(next); IIdType resourceId = getContainedResources().getResourceId(next);
encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, false, fixContainedResourceId(resourceId.getValue())); encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, false, fixContainedResourceId(resourceId.getValue()));
} }
theEventWriter.endArray(); theEventWriter.endArray();
}
break;
}
case PRIMITIVE_XHTML_HL7ORG:
case PRIMITIVE_XHTML: {
if (!isSuppressNarratives()) {
IPrimitiveType<?> dt = (IPrimitiveType<?>) theNextValue;
if (theChildName != null) {
write(theEventWriter, theChildName, dt.getValueAsString());
} else {
theEventWriter.write(dt.getValueAsString());
}
} else {
if (theChildName != null) {
// do nothing
} else {
theEventWriter.writeNull();
} }
break;
} }
break; case PRIMITIVE_XHTML_HL7ORG:
} case PRIMITIVE_XHTML: {
case RESOURCE: if (!isSuppressNarratives()) {
IBaseResource resource = (IBaseResource) theNextValue; IPrimitiveType<?> dt = (IPrimitiveType<?>) theNextValue;
RuntimeResourceDefinition def = myContext.getResourceDefinition(resource); if (theChildName != null) {
encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true); write(theEventWriter, theChildName, dt.getValueAsString());
break; } else {
case UNDECL_EXT: theEventWriter.write(dt.getValueAsString());
default: }
throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name()); } else {
if (theChildName != null) {
// do nothing
} else {
theEventWriter.writeNull();
}
}
break;
}
case RESOURCE:
IBaseResource resource = (IBaseResource) theNextValue;
RuntimeResourceDefinition def = myContext.getResourceDefinition(resource);
encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true);
break;
case UNDECL_EXT:
default:
throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name());
} }
} }
private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonLikeWriter theEventWriter, private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonLikeWriter theEventWriter,
boolean theContainedResource, boolean theSubResource, CompositeChildElement theParent) throws IOException { boolean theContainedResource, boolean theSubResource, CompositeChildElement theParent) throws IOException {
{ {
String elementId = getCompositeElementId(theElement); String elementId = getCompositeElementId(theElement);
@ -335,7 +340,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
BaseRuntimeChildDefinition nextChild = nextChildElem.getDef(); BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();
if (nextChildElem.getDef().getElementName().equals("extension") || nextChildElem.getDef().getElementName().equals("modifierExtension") if (nextChildElem.getDef().getElementName().equals("extension") || nextChildElem.getDef().getElementName().equals("modifierExtension")
|| nextChild instanceof RuntimeChildDeclaredExtensionDefinition) { || nextChild instanceof RuntimeChildDeclaredExtensionDefinition) {
if (!haveWrittenExtensions) { if (!haveWrittenExtensions) {
extractAndWriteExtensionsAsDirectChild(theElement, theEventWriter, myContext.getElementDefinition(theElement.getClass()), theResDef, theResource, nextChildElem, theParent); extractAndWriteExtensionsAsDirectChild(theElement, theEventWriter, myContext.getElementDefinition(theElement.getClass()), theResDef, theResource, nextChildElem, theParent);
haveWrittenExtensions = true; haveWrittenExtensions = true;
@ -451,15 +456,15 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
if (nextChild.getMax() > 1 || nextChild.getMax() == Child.MAX_UNLIMITED) { if (nextChild.getMax() > 1 || nextChild.getMax() == Child.MAX_UNLIMITED) {
beginArray(theEventWriter, childName); beginArray(theEventWriter, childName);
inArray = true; inArray = true;
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, theSubResource,nextChildElem, force); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, theSubResource, nextChildElem, force);
} else if (nextChild instanceof RuntimeChildNarrativeDefinition && theContainedResource) { } else if (nextChild instanceof RuntimeChildNarrativeDefinition && theContainedResource) {
// suppress narratives from contained resources // suppress narratives from contained resources
} else { } else {
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, childName, theContainedResource, theSubResource,nextChildElem, false); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, childName, theContainedResource, theSubResource, nextChildElem, false);
} }
currentChildName = childName; currentChildName = childName;
} else { } else {
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource,theSubResource, nextChildElem, force); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, theSubResource, nextChildElem, force);
} }
valueIdx++; valueIdx++;
@ -541,7 +546,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, boolean theSubResource, private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, boolean theSubResource,
CompositeChildElement theParent) throws IOException, DataFormatException { CompositeChildElement theParent) throws IOException, DataFormatException {
writeCommentsPreAndPost(theNextValue, theEventWriter); writeCommentsPreAndPost(theNextValue, theEventWriter);
encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theParent); encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theParent);
@ -554,14 +559,14 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) { if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum());
} }
doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter); doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter);
} }
private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull,
boolean theContainedResource, boolean theSubResource) throws IOException { boolean theContainedResource, boolean theSubResource) throws IOException {
IIdType resourceId = null; IIdType resourceId = null;
// if (theResource instanceof IResource) { // if (theResource instanceof IResource) {
// IResource res = (IResource) theResource; // IResource res = (IResource) theResource;
@ -598,7 +603,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull,
boolean theContainedResource, boolean theSubResource, IIdType theResourceId) throws IOException { boolean theContainedResource, boolean theSubResource, IIdType theResourceId) throws IOException {
if (!theContainedResource) { if (!theContainedResource) {
super.containResourcesForEncoding(theResource); super.containResourcesForEncoding(theResource);
} }
@ -612,28 +617,28 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
write(theEventWriter, "resourceType", resDef.getName()); write(theEventWriter, "resourceType", resDef.getName());
if (theResourceId != null && theResourceId.hasIdPart()) { if (theResourceId != null && theResourceId.hasIdPart()) {
write(theEventWriter, "id", theResourceId.getIdPart()); write(theEventWriter, "id", theResourceId.getIdPart());
final List<HeldExtension> extensions = new ArrayList<HeldExtension>(0); final List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
final List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0); final List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);
// Undeclared extensions // Undeclared extensions
extractUndeclaredExtensions(theResourceId, extensions, modifierExtensions, null, null); extractUndeclaredExtensions(theResourceId, extensions, modifierExtensions, null, null);
boolean haveExtension = false; boolean haveExtension = false;
if (!extensions.isEmpty()) { if (!extensions.isEmpty()) {
haveExtension = true; haveExtension = true;
} }
if (theResourceId.hasFormatComment() || haveExtension) { if (theResourceId.hasFormatComment() || haveExtension) {
beginObject(theEventWriter, "_id"); beginObject(theEventWriter, "_id");
if (theResourceId.hasFormatComment()) { if (theResourceId.hasFormatComment()) {
writeCommentsPreAndPost(theResourceId, theEventWriter); writeCommentsPreAndPost(theResourceId, theEventWriter);
} }
if (haveExtension) { if (haveExtension) {
writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions); writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions);
} }
theEventWriter.endObject(); theEventWriter.endObject();
} }
} }
if (theResource instanceof IResource) { if (theResource instanceof IResource) {
IResource resource = (IResource) theResource; IResource resource = (IResource) theResource;
@ -695,19 +700,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
} }
if (theResource instanceof IBaseBinary) { encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, theSubResource, new CompositeChildElement(resDef, theSubResource));
IBaseBinary bin = (IBaseBinary) theResource;
String contentType = bin.getContentType();
if (isNotBlank(contentType)) {
write(theEventWriter, "contentType", contentType);
}
String contentAsBase64 = bin.getContentAsBase64();
if (isNotBlank(contentAsBase64)) {
write(theEventWriter, "content", contentAsBase64);
}
} else {
encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, theSubResource, new CompositeChildElement(resDef, theSubResource));
}
theEventWriter.endObject(); theEventWriter.endObject();
} }
@ -715,12 +708,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
/** /**
* This is useful only for the two cases where extensions are encoded as direct children (e.g. not in some object * This is useful only for the two cases where extensions are encoded as direct children (e.g. not in some object
* called _name): resource extensions, and extension extensions * called _name): resource extensions, and extension extensions
* *
* @param theChildElem * @param theChildElem
* @param theParent * @param theParent
*/ */
private void extractAndWriteExtensionsAsDirectChild(IBase theElement, JsonLikeWriter theEventWriter, BaseRuntimeElementDefinition<?> theElementDef, RuntimeResourceDefinition theResDef, private void extractAndWriteExtensionsAsDirectChild(IBase theElement, JsonLikeWriter theEventWriter, BaseRuntimeElementDefinition<?> theElementDef, RuntimeResourceDefinition theResDef,
IBaseResource theResource, CompositeChildElement theChildElem, CompositeChildElement theParent) throws IOException { IBaseResource theResource, CompositeChildElement theChildElem, CompositeChildElement theParent) throws IOException {
List<HeldExtension> extensions = new ArrayList<HeldExtension>(0); List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0); List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);
@ -737,7 +730,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void extractDeclaredExtensions(IBase theResource, BaseRuntimeElementDefinition<?> resDef, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, private void extractDeclaredExtensions(IBase theResource, BaseRuntimeElementDefinition<?> resDef, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions,
CompositeChildElement theChildElem) { CompositeChildElement theChildElem) {
for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsNonModifier()) { for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsNonModifier()) {
for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) { for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) {
if (nextValue != null) { if (nextValue != null) {
@ -761,7 +754,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void extractUndeclaredExtensions(IBase theElement, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, CompositeChildElement theChildElem, private void extractUndeclaredExtensions(IBase theElement, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, CompositeChildElement theChildElem,
CompositeChildElement theParent) { CompositeChildElement theParent) {
if (theElement instanceof ISupportsUndeclaredExtensions) { if (theElement instanceof ISupportsUndeclaredExtensions) {
ISupportsUndeclaredExtensions element = (ISupportsUndeclaredExtensions) theElement; ISupportsUndeclaredExtensions element = (ISupportsUndeclaredExtensions) theElement;
List<ExtensionDt> ext = element.getUndeclaredExtensions(); List<ExtensionDt> ext = element.getUndeclaredExtensions();
@ -1029,78 +1022,78 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
} }
private void parseExtension(ParserState<?> theState, JsonLikeArray theValues, boolean theIsModifier) { private void parseExtension(ParserState<?> theState, JsonLikeArray theValues, boolean theIsModifier) {
int allUnderscoreNames = 0; int allUnderscoreNames = 0;
int handledUnderscoreNames = 0; int handledUnderscoreNames = 0;
for (int i = 0; i < theValues.size(); i++) {
JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i));
JsonLikeValue jsonElement = nextExtObj.get("url");
String url;
if (null == jsonElement || !(jsonElement.isScalar())) {
String parentElementName;
if (theIsModifier) {
parentElementName = "modifierExtension";
} else {
parentElementName = "extension";
}
getErrorHandler().missingRequiredElement(new ParseLocation(parentElementName), "url");
url = null;
} else {
url = getExtensionUrl(jsonElement.getAsString());
}
theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl());
for (String next : nextExtObj.keySet()) {
if ("url".equals(next)) {
continue;
} else if ("extension".equals(next)) {
JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
parseExtension(theState, jsonVal, false);
} else if ("modifierExtension".equals(next)) {
JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
parseExtension(theState, jsonVal, true);
} else if (next.charAt(0) == '_') {
allUnderscoreNames++;
continue;
} else {
JsonLikeValue jsonVal = nextExtObj.get(next);
String alternateName = '_' + next;
JsonLikeValue alternateVal = nextExtObj.get(alternateName);
if (alternateVal != null) {
handledUnderscoreNames++;
}
parseChildren(theState, next, jsonVal, alternateVal, alternateName, false);
}
}
for (int i = 0; i < theValues.size(); i++) {
JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i));
JsonLikeValue jsonElement = nextExtObj.get("url");
String url;
if (null == jsonElement || !(jsonElement.isScalar())) {
String parentElementName;
if (theIsModifier) {
parentElementName = "modifierExtension";
} else {
parentElementName = "extension";
}
getErrorHandler().missingRequiredElement(new ParseLocation(parentElementName), "url");
url = null;
} else {
url = getExtensionUrl(jsonElement.getAsString());
}
theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl());
for (String next : nextExtObj.keySet()) {
if ("url".equals(next)) {
continue;
} else if ("extension".equals(next)) {
JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
parseExtension(theState, jsonVal, false);
} else if ("modifierExtension".equals(next)) {
JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
parseExtension(theState, jsonVal, true);
} else if (next.charAt(0) == '_') {
allUnderscoreNames++;
continue;
} else {
JsonLikeValue jsonVal = nextExtObj.get(next);
String alternateName = '_' + next;
JsonLikeValue alternateVal = nextExtObj.get(alternateName);
if (alternateVal != null) {
handledUnderscoreNames++;
}
parseChildren(theState, next, jsonVal, alternateVal, alternateName, false);
}
}
/* /*
* This happens if an element has an extension but no actual value. I.e. * This happens if an element has an extension but no actual value. I.e.
* if a resource has a "_status" element but no corresponding "status" * if a resource has a "_status" element but no corresponding "status"
* element. This could be used to handle a null value with an extension * element. This could be used to handle a null value with an extension
* for example. * for example.
*/ */
if (allUnderscoreNames > handledUnderscoreNames) { if (allUnderscoreNames > handledUnderscoreNames) {
for (String alternateName : nextExtObj.keySet()) { for (String alternateName : nextExtObj.keySet()) {
if (alternateName.startsWith("_") && alternateName.length() > 1) { if (alternateName.startsWith("_") && alternateName.length() > 1) {
JsonLikeValue nextValue = nextExtObj.get(alternateName); JsonLikeValue nextValue = nextExtObj.get(alternateName);
if (nextValue != null) { if (nextValue != null) {
if (nextValue.isObject()) { if (nextValue.isObject()) {
String nextName = alternateName.substring(1); String nextName = alternateName.substring(1);
if (nextExtObj.get(nextName) == null) { if (nextExtObj.get(nextName) == null) {
theState.enteringNewElement(null, nextName); theState.enteringNewElement(null, nextName);
parseAlternates(nextValue, theState, alternateName, alternateName); parseAlternates(nextValue, theState, alternateName, alternateName);
theState.endingElement(); theState.endingElement();
} }
} else { } else {
getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null); getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null);
} }
} }
} }
} }
} }
theState.endingElement(); theState.endingElement();
} }
} }
private void parseFhirComments(JsonLikeValue theObject, ParserState<?> theState) { private void parseFhirComments(JsonLikeValue theObject, ParserState<?> theState) {
if (theObject.isArray()) { if (theObject.isArray()) {
@ -1254,7 +1247,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} }
private void writeExtensionsAsDirectChild(IBaseResource theResource, JsonLikeWriter theEventWriter, RuntimeResourceDefinition resDef, List<HeldExtension> extensions, private void writeExtensionsAsDirectChild(IBaseResource theResource, JsonLikeWriter theEventWriter, RuntimeResourceDefinition resDef, List<HeldExtension> extensions,
List<HeldExtension> modifierExtensions) throws IOException { List<HeldExtension> modifierExtensions) throws IOException {
if (extensions.isEmpty() == false) { if (extensions.isEmpty() == false) {
beginArray(theEventWriter, "extension"); beginArray(theEventWriter, "extension");
for (HeldExtension next : extensions) { for (HeldExtension next : extensions) {
@ -1328,6 +1321,28 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
return url1.compareTo(url2); return url1.compareTo(url2);
} }
private void managePrimitiveExtension(final IBase theValue, final RuntimeResourceDefinition theResDef, final IBaseResource theResource, final JsonLikeWriter theEventWriter, final BaseRuntimeElementDefinition<?> def, final String childName) throws IOException {
if (def.getChildType().equals(ID_DATATYPE) || def.getChildType().equals(PRIMITIVE_DATATYPE)) {
final List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
final List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);
// Undeclared extensions
extractUndeclaredExtensions(theValue, extensions, modifierExtensions, myParent, null);
// Declared extensions
if (def != null) {
extractDeclaredExtensions(theValue, def, extensions, modifierExtensions, myParent);
}
boolean haveContent = false;
if (!extensions.isEmpty() || !modifierExtensions.isEmpty()) {
haveContent = true;
}
if (haveContent) {
beginObject(theEventWriter, '_' + childName);
writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions);
theEventWriter.endObject();
}
}
}
public void write(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException { public void write(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException {
if (myUndeclaredExtension != null) { if (myUndeclaredExtension != null) {
writeUndeclaredExtension(theResDef, theResource, theEventWriter, myUndeclaredExtension); writeUndeclaredExtension(theResDef, theResource, theEventWriter, myUndeclaredExtension);
@ -1341,7 +1356,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
/* /*
* This makes sure that even if the extension contains a reference to a contained * This makes sure that even if the extension contains a reference to a contained
* resource which has a HAPI-assigned ID we'll still encode that ID. * resource which has a HAPI-assigned ID we'll still encode that ID.
* *
* See #327 * See #327
*/ */
List<? extends IBase> preProcessedValue = preProcessValues(myDef, theResource, Collections.singletonList(myValue), myChildElem); List<? extends IBase> preProcessedValue = preProcessValues(myDef, theResource, Collections.singletonList(myValue), myChildElem);
@ -1367,7 +1382,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} else { } else {
String childName = myDef.getChildNameByDatatype(myValue.getClass()); String childName = myDef.getChildNameByDatatype(myValue.getClass());
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, false, myParent, false); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, false, myParent, false);
managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName); managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName);
} }
theEventWriter.endObject(); theEventWriter.endObject();
@ -1422,34 +1437,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + value.getClass().getCanonicalName()); throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + value.getClass().getCanonicalName());
} }
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true, false, myParent, false); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true, false, myParent, false);
managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName); managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName);
} }
// theEventWriter.name(myUndeclaredExtension.get); // theEventWriter.name(myUndeclaredExtension.get);
theEventWriter.endObject(); theEventWriter.endObject();
} }
}
private void managePrimitiveExtension(final IBase theValue, final RuntimeResourceDefinition theResDef, final IBaseResource theResource, final JsonLikeWriter theEventWriter, final BaseRuntimeElementDefinition<?> def, final String childName) throws IOException {
if (def.getChildType().equals(ID_DATATYPE) || def.getChildType().equals(PRIMITIVE_DATATYPE)) {
final List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
final List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);
// Undeclared extensions
extractUndeclaredExtensions(theValue, extensions, modifierExtensions, myParent, null);
// Declared extensions
if (def != null) {
extractDeclaredExtensions(theValue, def, extensions, modifierExtensions, myParent);
}
boolean haveContent = false;
if (!extensions.isEmpty() || !modifierExtensions.isEmpty()) {
haveContent = true;
}
if (haveContent) {
beginObject(theEventWriter, '_' + childName);
writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions);
theEventWriter.endObject();
}
}
}
}
} }

View File

@ -606,7 +606,7 @@ class ParserState<T> {
return; return;
} }
case RESOURCE: { case RESOURCE: {
if (myInstance instanceof IAnyResource || myInstance instanceof IBaseBackboneElement) { if (myInstance instanceof IAnyResource || myInstance instanceof IBaseBackboneElement || myInstance instanceof IBaseElement) {
ParserState<T>.PreResourceStateHl7Org state = new PreResourceStateHl7Org(myInstance, child.getMutator(), null); ParserState<T>.PreResourceStateHl7Org state = new PreResourceStateHl7Org(myInstance, child.getMutator(), null);
push(state); push(state);
} else { } else {
@ -1559,7 +1559,8 @@ class ParserState<T> {
if (theEvent.isEndElement()) { if (theEvent.isEndElement()) {
if (myDepth == 0) { if (myDepth == 0) {
myDt.setValue(myEvents); String eventsAsString = XmlUtil.encode(myEvents);
myDt.setValue(eventsAsString);
doPop(); doPop();
} }
} }

View File

@ -24,6 +24,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.Reader; import java.io.Reader;
import java.io.StringReader;
import java.io.Writer; import java.io.Writer;
import java.util.*; import java.util.*;
@ -605,13 +606,16 @@ public class XmlParser extends BaseParser /* implements IParser */ {
} }
} }
private void encodeXhtml(XhtmlDt theDt, XMLStreamWriter theEventWriter) throws XMLStreamException { private void encodeXhtml(XhtmlDt theDt, XMLStreamWriter theEventWriter) throws XMLStreamException {
if (theDt == null || theDt.getValue() == null) { if (theDt == null || theDt.getValue() == null) {
return; return;
} }
List<XMLEvent> events = XmlUtil.parse(theDt.getValue());
boolean firstElement = true; boolean firstElement = true;
for (XMLEvent event : theDt.getValue()) {
for (XMLEvent event : events) {
switch (event.getEventType()) { switch (event.getEventType()) {
case XMLStreamConstants.ATTRIBUTE: case XMLStreamConstants.ATTRIBUTE:
Attribute attr = (Attribute) event; Attribute attr = (Attribute) event;

View File

@ -31,6 +31,16 @@ public class Constants {
public static final String CHARSET_NAME_UTF8 = "UTF-8"; public static final String CHARSET_NAME_UTF8 = "UTF-8";
public static final Charset CHARSET_UTF8; public static final Charset CHARSET_UTF8;
public static final String CHARSET_UTF8_CTSUFFIX = "; charset=" + CHARSET_NAME_UTF8; public static final String CHARSET_UTF8_CTSUFFIX = "; charset=" + CHARSET_NAME_UTF8;
/**
* Contains a standard set of headers which are used by FHIR / HAPI FHIR, and therefore
* would make a useful set for CORS AllowedHeader declarations
*/
public static final Set<String> CORS_ALLOWED_HEADERS;
/**
* Contains a standard set of HTTP Methods which are used by FHIR / HAPI FHIR, and therefore
* would make a useful set for CORS AllowedMethod declarations
*/
public static final Set<String> CORS_ALLWED_METHODS;
public static final String CT_FHIR_JSON = "application/json+fhir"; public static final String CT_FHIR_JSON = "application/json+fhir";
public static final String CT_FHIR_JSON_NEW = "application/fhir+json"; public static final String CT_FHIR_JSON_NEW = "application/fhir+json";
public static final String CT_FHIR_XML = "application/xml+fhir"; public static final String CT_FHIR_XML = "application/xml+fhir";
@ -176,12 +186,13 @@ public class Constants {
public static final String URL_TOKEN_METADATA = "metadata"; public static final String URL_TOKEN_METADATA = "metadata";
public static final String OO_INFOSTATUS_PROCESSING = "processing"; public static final String OO_INFOSTATUS_PROCESSING = "processing";
public static final String PARAM_GRAPHQL_QUERY = "query"; public static final String PARAM_GRAPHQL_QUERY = "query";
public static final String HEADER_X_CACHE = "X-Cache";
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
static { static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
HashMap<Integer, String> statusNames = new HashMap<Integer, String>(); HashMap<Integer, String> statusNames = new HashMap<>();
statusNames.put(200, "OK"); statusNames.put(200, "OK");
statusNames.put(201, "Created"); statusNames.put(201, "Created");
statusNames.put(202, "Accepted"); statusNames.put(202, "Accepted");
@ -246,11 +257,31 @@ public class Constants {
statusNames.put(511, "Network Authentication Required"); statusNames.put(511, "Network Authentication Required");
HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames); HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames);
Set<String> formatsHtml = new HashSet<String>(); Set<String> formatsHtml = new HashSet<>();
formatsHtml.add(CT_HTML); formatsHtml.add(CT_HTML);
formatsHtml.add(FORMAT_HTML); formatsHtml.add(FORMAT_HTML);
FORMATS_HTML = Collections.unmodifiableSet(formatsHtml); FORMATS_HTML = Collections.unmodifiableSet(formatsHtml);
// *********************************************************
// Update CorsInterceptor's constructor documentation if you change these:
// *********************************************************
HashSet<String> corsAllowedHeaders = new HashSet<>();
corsAllowedHeaders.add("Accept");
corsAllowedHeaders.add("Access-Control-Request-Headers");
corsAllowedHeaders.add("Access-Control-Request-Method");
corsAllowedHeaders.add("Cache-Control");
corsAllowedHeaders.add("Content-Type");
corsAllowedHeaders.add("Origin");
corsAllowedHeaders.add("Prefer");
corsAllowedHeaders.add("X-Requested-With");
CORS_ALLOWED_HEADERS = Collections.unmodifiableSet(corsAllowedHeaders);
// *********************************************************
// Update CorsInterceptor's constructor documentation if you change these:
// *********************************************************
HashSet<String> corsAllowedMethods = new HashSet<>();
corsAllowedMethods.addAll(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
CORS_ALLWED_METHODS = Collections.unmodifiableSet(corsAllowedMethods);
} }
} }

View File

@ -35,7 +35,7 @@ public interface IVersionSpecificBundleFactory {
void addResourcesToBundle(List<IBaseResource> theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set<Include> theIncludes); void addResourcesToBundle(List<IBaseResource> theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set<Include> theIncludes);
void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theLinkSelf, String theLinkPrev, String theLinkNext, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType<Date> theLastUpdated); void addRootPropertiesToBundle(String theId, String theServerBase, String theLinkSelf, String theLinkPrev, String theLinkNext, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType<Date> theLastUpdated);
IBaseResource getResourceBundle(); IBaseResource getResourceBundle();

View File

@ -0,0 +1,47 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseReference;
import java.util.List;
public class BinaryUtil {
private BinaryUtil() {
// non instantiable
}
public static IBaseReference getSecurityContext(FhirContext theCtx, IBaseBinary theBinary) {
RuntimeResourceDefinition def = theCtx.getResourceDefinition("Binary");
BaseRuntimeChildDefinition child = def.getChildByName("securityContext");
IBaseReference retVal = null;
if (child != null) {
List<IBase> values = child.getAccessor().getValues(theBinary);
if (values.size() > 0) {
retVal = (IBaseReference) values.get(0);
}
}
return retVal;
}
public static IBaseBinary newBinary(FhirContext theCtx) {
return (IBaseBinary) theCtx.getResourceDefinition("Binary").newInstance();
}
public static void setSecurityContext(FhirContext theCtx, IBaseBinary theBinary, String theSecurityContext) {
RuntimeResourceDefinition def = theCtx.getResourceDefinition("Binary");
BaseRuntimeChildDefinition child = def.getChildByName("securityContext");
BaseRuntimeElementDefinition<?> referenceDef = theCtx.getElementDefinition("reference");
IBaseReference reference = (IBaseReference) referenceDef.newInstance();
child.getMutator().addValue(theBinary, reference);
reference.setReference(theSecurityContext);
}
}

View File

@ -21,12 +21,14 @@ package ca.uhn.fhir.util;
*/ */
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.LinkedHashSet;
/** /**
* Provides server ports * Provides server ports
*/ */
@CoverageIgnore @CoverageIgnore
public class PortUtil { public class PortUtil {
private static LinkedHashSet<Integer> ourPorts = new LinkedHashSet<>();
/* /*
* Non instantiable * Non instantiable
@ -41,9 +43,13 @@ public class PortUtil {
public static int findFreePort() { public static int findFreePort() {
ServerSocket server; ServerSocket server;
try { try {
server = new ServerSocket(0); int port;
int port = server.getLocalPort(); do {
server.close(); server = new ServerSocket(0);
port = server.getLocalPort();
server.close();
} while (!ourPorts.add(port));
Thread.sleep(500); Thread.sleep(500);
return port; return port;
} catch (Exception e) { } catch (Exception e) {

View File

@ -20,33 +20,34 @@ package ca.uhn.fhir.util;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import org.slf4j.LoggerFactory; import static org.apache.commons.lang3.StringUtils.defaultString;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
public class TestUtil { public class TestUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
/** /**
* <b>THIS IS FOR UNIT TESTS ONLY - DO NOT CALL THIS METHOD FROM USER CODE</b> * <b>THIS IS FOR UNIT TESTS ONLY - DO NOT CALL THIS METHOD FROM USER CODE</b>
* * <p>
* When we run the unit tests in cobertura, JUnit doesn't seem to clean up static fields which leads to * When we run the unit tests in cobertura, JUnit doesn't seem to clean up static fields which leads to
* tons of memory being used by the end and the JVM crashes in Travis. Manually clearing all of the * tons of memory being used by the end and the JVM crashes in Travis. Manually clearing all of the
* static fields seems to solve this. * static fields seems to solve this.
*/ */
public static void clearAllStaticFieldsForUnitTest() { public static void clearAllStaticFieldsForUnitTest() {
HapiLocalizer.setOurFailOnMissingMessage(true); HapiLocalizer.setOurFailOnMissingMessage(true);
Class<?> theType; Class<?> theType;
try { try {
throw new Exception(); throw new Exception();
@ -104,7 +105,7 @@ public class TestUtil {
* environment * environment
*/ */
public static void randomizeLocale() { public static void randomizeLocale() {
Locale[] availableLocales = { Locale.CANADA, Locale.GERMANY, Locale.TAIWAN }; Locale[] availableLocales = {Locale.CANADA, Locale.GERMANY, Locale.TAIWAN};
Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]); Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]);
ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName()); ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName());
if (Math.random() < 0.5) { if (Math.random() < 0.5) {
@ -116,10 +117,19 @@ public class TestUtil {
System.setProperty("file.encoding", "UTF-8"); System.setProperty("file.encoding", "UTF-8");
System.setProperty("line.separator", "\n"); System.setProperty("line.separator", "\n");
} }
String availableTimeZones[] = { "GMT+08:00", "GMT-05:00", "GMT+00:00", "GMT+03:30" }; String availableTimeZones[] = {"GMT+08:00", "GMT-05:00", "GMT+00:00", "GMT+03:30"};
String timeZone = availableTimeZones[(int) (Math.random() * availableTimeZones.length)]; String timeZone = availableTimeZones[(int) (Math.random() * availableTimeZones.length)];
TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
ourLog.info("Tests are using time zone: {}", TimeZone.getDefault().getID()); ourLog.info("Tests are using time zone: {}", TimeZone.getDefault().getID());
} }
/**
* <b>THIS IS FOR UNIT TESTS ONLY - DO NOT CALL THIS METHOD FROM USER CODE</b>
* <p>
* Strip \r chars from a string to account for line ending platform differences
*/
public static String stripReturns(String theString) {
return defaultString(theString).replace("\r", "");
}
} }

View File

@ -0,0 +1,57 @@
package ca.uhn.fhir.util;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Reader;
import java.io.StringReader;
public class XmlDetectionUtil {
private static final Logger ourLog = LoggerFactory.getLogger(XmlDetectionUtil.class);
private static Boolean ourStaxPresent;
/**
* This method will return <code>true</code> if a StAX XML parsing library is present
* on the classpath
*/
public static boolean isStaxPresent() {
Boolean retVal = ourStaxPresent;
if (retVal == null) {
try {
Class.forName("javax.xml.stream.events.XMLEvent");
Class<?> xmlUtilClazz = Class.forName("ca.uhn.fhir.util.XmlUtil");
xmlUtilClazz.getMethod("createXmlReader", Reader.class).invoke(xmlUtilClazz, new StringReader(""));
ourStaxPresent = Boolean.TRUE;
retVal = Boolean.TRUE;
} catch (Throwable t) {
ourLog.info("StAX not detected on classpath, XML processing will be disabled");
ourStaxPresent = Boolean.FALSE;
retVal = Boolean.FALSE;
}
}
return retVal;
}
}

View File

@ -20,12 +20,13 @@ package ca.uhn.fhir.util;
* #L% * #L%
*/ */
import java.io.*; import java.io.*;
import java.util.Collections; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import javax.xml.stream.*; import javax.xml.stream.*;
import javax.xml.stream.events.XMLEvent;
import ca.uhn.fhir.model.primitive.XhtmlDt;
import ca.uhn.fhir.parser.DataFormatException;
import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringEscapeUtils;
import org.codehaus.stax2.XMLOutputFactory2; import org.codehaus.stax2.XMLOutputFactory2;
import org.codehaus.stax2.io.EscapingWriterFactory; import org.codehaus.stax2.io.EscapingWriterFactory;
@ -37,21 +38,22 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.util.jar.DependencyLogFactory; import ca.uhn.fhir.util.jar.DependencyLogFactory;
import ca.uhn.fhir.util.jar.IDependencyLog; import ca.uhn.fhir.util.jar.IDependencyLog;
import static org.apache.commons.lang3.StringUtils.isBlank;
/** /**
* Utility methods for working with the StAX API. * Utility methods for working with the StAX API.
* *
* This class contains code adapted from the Apache Axiom project. * This class contains code adapted from the Apache Axiom project.
*/ */
public class XmlUtil { public class XmlUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlUtil.class);
private static final Map<String, Integer> VALID_ENTITY_NAMES;
private static final ExtendedEntityReplacingXmlResolver XML_RESOLVER = new ExtendedEntityReplacingXmlResolver();
private static XMLOutputFactory ourFragmentOutputFactory; private static XMLOutputFactory ourFragmentOutputFactory;
private static volatile boolean ourHaveLoggedStaxImplementation; private static volatile boolean ourHaveLoggedStaxImplementation;
private static volatile XMLInputFactory ourInputFactory; private static volatile XMLInputFactory ourInputFactory;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlUtil.class);
private static Throwable ourNextException; private static Throwable ourNextException;
private static volatile XMLOutputFactory ourOutputFactory; private static volatile XMLOutputFactory ourOutputFactory;
private static Boolean ourStaxPresent;
private static final Map<String, Integer> VALID_ENTITY_NAMES;
private static final ExtendedEntityReplacingXmlResolver XML_RESOLVER = new ExtendedEntityReplacingXmlResolver();
static { static {
HashMap<String, Integer> validEntityNames = new HashMap<String, Integer>(1448); HashMap<String, Integer> validEntityNames = new HashMap<String, Integer>(1448);
@ -1517,7 +1519,7 @@ public class XmlUtil {
} }
XMLOutputFactory outputFactory = newOutputFactory(); XMLOutputFactory outputFactory = newOutputFactory();
if (!ourHaveLoggedStaxImplementation) { if (!ourHaveLoggedStaxImplementation) {
logStaxImplementation(outputFactory.getClass()); logStaxImplementation(outputFactory.getClass());
} }
@ -1545,7 +1547,7 @@ public class XmlUtil {
public static XMLEventReader createXmlReader(Reader reader) throws FactoryConfigurationError, XMLStreamException { public static XMLEventReader createXmlReader(Reader reader) throws FactoryConfigurationError, XMLStreamException {
throwUnitTestExceptionIfConfiguredToDoSo(); throwUnitTestExceptionIfConfiguredToDoSo();
XMLInputFactory inputFactory = getOrCreateInputFactory(); XMLInputFactory inputFactory = getOrCreateInputFactory();
// Now.. create the reader and return it // Now.. create the reader and return it
@ -1555,7 +1557,7 @@ public class XmlUtil {
public static XMLStreamWriter createXmlStreamWriter(Writer theWriter) throws FactoryConfigurationError, XMLStreamException { public static XMLStreamWriter createXmlStreamWriter(Writer theWriter) throws FactoryConfigurationError, XMLStreamException {
throwUnitTestExceptionIfConfiguredToDoSo(); throwUnitTestExceptionIfConfiguredToDoSo();
XMLOutputFactory outputFactory = getOrCreateOutputFactory(); XMLOutputFactory outputFactory = getOrCreateOutputFactory();
XMLStreamWriter retVal = outputFactory.createXMLStreamWriter(theWriter); XMLStreamWriter retVal = outputFactory.createXMLStreamWriter(theWriter);
return retVal; return retVal;
@ -1567,6 +1569,30 @@ public class XmlUtil {
return retVal; return retVal;
} }
/**
* Encode a set of StAX events into a String
*/
public static String encode(List<XMLEvent> theEvents) {
try {
StringWriter w = new StringWriter();
XMLEventWriter ew = XmlUtil.createXmlFragmentWriter(w);
for (XMLEvent next : theEvents) {
if (next.isCharacters()) {
ew.add(next);
} else {
ew.add(next);
}
}
ew.close();
return w.toString();
} catch (XMLStreamException e) {
throw new DataFormatException("Problem with the contained XML events", e);
} catch (FactoryConfigurationError e) {
throw new ConfigurationException(e);
}
}
private static XMLOutputFactory getOrCreateFragmentOutputFactory() throws FactoryConfigurationError { private static XMLOutputFactory getOrCreateFragmentOutputFactory() throws FactoryConfigurationError {
XMLOutputFactory retVal = ourFragmentOutputFactory; XMLOutputFactory retVal = ourFragmentOutputFactory;
if (retVal == null) { if (retVal == null) {
@ -1588,7 +1614,7 @@ public class XmlUtil {
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// ok // ok
} }
XMLInputFactory inputFactory = newInputFactory(); XMLInputFactory inputFactory = newInputFactory();
if (!ourHaveLoggedStaxImplementation) { if (!ourHaveLoggedStaxImplementation) {
@ -1596,20 +1622,20 @@ public class XmlUtil {
} }
/* /*
* These two properties disable external entity processing, which can * These two properties disable external entity processing, which can
* be a security vulnerability. * be a security vulnerability.
* *
* See https://github.com/jamesagnew/hapi-fhir/issues/339 * See https://github.com/jamesagnew/hapi-fhir/issues/339
* https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing * https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing
*/ */
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // This disables DTDs entirely for that factory inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // This disables DTDs entirely for that factory
inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); // disable external entities inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); // disable external entities
/* /*
* In the following few lines, you can uncomment the first and comment the second to disable automatic * In the following few lines, you can uncomment the first and comment the second to disable automatic
* parsing of extended entities, e.g. &sect; * parsing of extended entities, e.g. &sect;
* *
* Note that these properties are Woodstox specific and they cause a crash in environments where SJSXP is * Note that these properties are Woodstox specific and they cause a crash in environments where SJSXP is
* being used (e.g. glassfish) so we don't set them there. * being used (e.g. glassfish) so we don't set them there.
*/ */
@ -1652,6 +1678,7 @@ public class XmlUtil {
return ourOutputFactory; return ourOutputFactory;
} }
private static void logStaxImplementation(Class<?> theClass) { private static void logStaxImplementation(Class<?> theClass) {
IDependencyLog logger = DependencyLogFactory.createJarLogger(); IDependencyLog logger = DependencyLogFactory.createJarLogger();
if (logger != null) { if (logger != null) {
@ -1683,6 +1710,49 @@ public class XmlUtil {
return outputFactory; return outputFactory;
} }
/**
* Parses an XML string into a set of StAX events
*/
public static List<XMLEvent> parse(String theValue) {
if (isBlank(theValue)) {
return Collections.emptyList();
}
String val = theValue.trim();
if (!val.startsWith("<")) {
val = XhtmlDt.DIV_OPEN_FIRST + val + "</div>";
}
boolean hasProcessingInstruction = val.startsWith("<?");
if (hasProcessingInstruction && val.endsWith("?>")) {
return null;
}
try {
ArrayList<XMLEvent> value = new ArrayList<>();
StringReader reader = new StringReader(val);
XMLEventReader er = XmlUtil.createXmlReader(reader);
boolean first = true;
while (er.hasNext()) {
XMLEvent next = er.nextEvent();
if (first) {
first = false;
continue;
}
if (er.hasNext()) {
// don't add the last event
value.add(next);
}
}
return value;
} catch (XMLStreamException e) {
throw new DataFormatException("String does not appear to be valid XML/XHTML (error is \"" + e.getMessage() + "\"): " + theValue, e);
} catch (FactoryConfigurationError e) {
throw new ConfigurationException(e);
}
}
/** /**
* FOR UNIT TESTS ONLY - Throw this exception for the next operation * FOR UNIT TESTS ONLY - Throw this exception for the next operation
*/ */
@ -1711,26 +1781,6 @@ public class XmlUtil {
return null; return null;
} }
} }
/**
* This method will return <code>true</code> if a StAX XML parsing library is present
* on the classpath
*/
public static boolean isStaxPresent() {
Boolean retVal = ourStaxPresent;
if (retVal == null) {
try {
newInputFactory();
ourStaxPresent = Boolean.TRUE;
retVal = Boolean.TRUE;
} catch (ConfigurationException e) {
ourLog.info("StAX not detected on classpath, XML processing will be disabled");
ourStaxPresent = Boolean.FALSE;
retVal = Boolean.FALSE;
}
}
return retVal;
}
public static class MyEscaper implements EscapingWriterFactory { public static class MyEscaper implements EscapingWriterFactory {

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class DefinitionException extends FHIRException {
private static final long serialVersionUID = 8175803009974854088L;
public DefinitionException() {
super();
}
public DefinitionException(String message, Throwable cause) {
super(message, cause);
}
public DefinitionException(String message) {
super(message);
}
public DefinitionException(Throwable cause) {
super(cause);
}
}

View File

@ -1,46 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class FHIRException extends Exception {
// Note that the 4-argument constructor has been removed as it is not JDK6 compatible
private static final long serialVersionUID = -1793365096090608037L;
public FHIRException() {
super();
}
public FHIRException(String message, Throwable cause) {
super(message, cause);
}
public FHIRException(String message) {
super(message);
}
public FHIRException(Throwable cause) {
super(cause);
}
}

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class FHIRFormatError extends FHIRException {
private static final long serialVersionUID = 1L;
public FHIRFormatError() {
super();
}
public FHIRFormatError(String message, Throwable cause) {
super(message, cause);
}
public FHIRFormatError(String message) {
super(message);
}
public FHIRFormatError(Throwable cause) {
super(cause);
}
}

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class NoTerminologyServiceException extends FHIRException {
private static final long serialVersionUID = 8196224236579018584L;
public NoTerminologyServiceException() {
super();
}
public NoTerminologyServiceException(String message, Throwable cause) {
super(message, cause);
}
public NoTerminologyServiceException(String message) {
super(message);
}
public NoTerminologyServiceException(Throwable cause) {
super(cause);
}
}

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class PathEngineException extends FHIRException {
private static final long serialVersionUID = -8550234068917355391L;
public PathEngineException() {
super();
}
public PathEngineException(String message, Throwable cause) {
super(message, cause);
}
public PathEngineException(String message) {
super(message);
}
public PathEngineException(Throwable cause) {
super(cause);
}
}

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class TerminologyServiceException extends FHIRException {
private static final long serialVersionUID = -1547510329455394225L;
public TerminologyServiceException() {
super();
}
public TerminologyServiceException(String message, Throwable cause) {
super(message, cause);
}
public TerminologyServiceException(String message) {
super(message);
}
public TerminologyServiceException(Throwable cause) {
super(cause);
}
}

View File

@ -1,44 +0,0 @@
package org.hl7.fhir.exceptions;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class UcumException extends FHIRException {
private static final long serialVersionUID = -8535757334881835619L;
public UcumException() {
super();
}
public UcumException(String message, Throwable cause) {
super(message, cause);
}
public UcumException(String message) {
super(message);
}
public UcumException(Throwable cause) {
super(cause);
}
}

View File

@ -73,6 +73,7 @@ public class IgPackUploader extends BaseCommand {
.and(StructureDefinition.URL.matches().value(nextResourceUrl)) .and(StructureDefinition.URL.matches().value(nextResourceUrl))
.execute(); .execute();
} }
break;
default: default:
throw new ParseException("This command does not support FHIR version " + ctx.getVersion().getVersion()); throw new ParseException("This command does not support FHIR version " + ctx.getVersion().getVersion());
} }

View File

@ -1,28 +1,36 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
import java.util.*;
import javax.servlet.ServletException;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.cors.CorsConfiguration;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
import ca.uhn.fhir.jpa.provider.dstu3.*; import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.r4.*; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.*; import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
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.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.cors.CorsConfiguration;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class JpaServerDemo extends RestfulServer { public class JpaServerDemo extends RestfulServer {
@ -134,19 +142,7 @@ public class JpaServerDemo extends RestfulServer {
setPagingProvider(new FifoMemoryPagingProvider(10)); setPagingProvider(new FifoMemoryPagingProvider(10));
// Register a CORS filter // Register a CORS filter
CorsConfiguration config = new CorsConfiguration(); CorsInterceptor corsInterceptor = new CorsInterceptor();
CorsInterceptor corsInterceptor = new CorsInterceptor(config);
config.addAllowedHeader("x-fhir-starter");
config.addAllowedHeader("Origin");
config.addAllowedHeader("Accept");
config.addAllowedHeader("X-Requested-With");
config.addAllowedHeader("Content-Type");
config.addAllowedHeader("Access-Control-Request-Method");
config.addAllowedHeader("Access-Control-Request-Headers");
config.addAllowedOrigin("*");
config.addExposedHeader("Location");
config.addExposedHeader("Content-Location");
config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
registerInterceptor(corsInterceptor); registerInterceptor(corsInterceptor);
/* /*

View File

@ -59,6 +59,11 @@
</dependency> </dependency>
<!-- Unit test dependencies --> <!-- Unit test dependencies -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
@ -125,4 +130,4 @@
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@ -98,24 +98,23 @@ public class ApacheHttpResponse implements IHttpResponse {
} }
if (charset == null) { if (charset == null) {
if (Constants.STATUS_HTTP_204_NO_CONTENT != myResponse.getStatusLine().getStatusCode()) { if (Constants.STATUS_HTTP_204_NO_CONTENT != myResponse.getStatusLine().getStatusCode()) {
ourLog.warn("Response did not specify a charset."); ourLog.debug("Response did not specify a charset, defaulting to utf-8");
} }
charset = Charset.forName("UTF-8"); charset = Charset.forName("UTF-8");
} }
Reader reader = new InputStreamReader(readEntity(), charset); return new InputStreamReader(readEntity(), charset);
return reader;
} }
@Override @Override
public Map<String, List<String>> getAllHeaders() { public Map<String, List<String>> getAllHeaders() {
Map<String, List<String>> headers = new HashMap<String, List<String>>(); Map<String, List<String>> headers = new HashMap<>();
if (myResponse.getAllHeaders() != null) { if (myResponse.getAllHeaders() != null) {
for (Header next : myResponse.getAllHeaders()) { for (Header next : myResponse.getAllHeaders()) {
String name = next.getName().toLowerCase(); String name = next.getName().toLowerCase();
List<String> list = headers.get(name); List<String> list = headers.get(name);
if (list == null) { if (list == null) {
list = new ArrayList<String>(); list = new ArrayList<>();
headers.put(name, list); headers.put(name, list);
} }
list.add(next.getValue()); list.add(next.getValue());
@ -131,7 +130,7 @@ public class ApacheHttpResponse implements IHttpResponse {
if (headers == null) { if (headers == null) {
headers = new Header[0]; headers = new Header[0];
} }
List<String> retVal = new ArrayList<String>(); List<String> retVal = new ArrayList<>();
for (Header next : headers) { for (Header next : headers) {
retVal.add(next.getValue()); retVal.add(next.getValue());
} }

View File

@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.util.XmlDetectionUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
@ -110,7 +111,7 @@ public abstract class BaseClient implements IRestfulClient {
setKeepResponses(true); setKeepResponses(true);
} }
if (XmlUtil.isStaxPresent() == false) { if (XmlDetectionUtil.isStaxPresent() == false) {
myEncoding = EncodingEnum.JSON; myEncoding = EncodingEnum.JSON;
} }

View File

@ -330,6 +330,8 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
serverFhirVersionEnum = FhirVersionEnum.DSTU2_1; serverFhirVersionEnum = FhirVersionEnum.DSTU2_1;
} else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU3.getFhirVersionString())) { } else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU3.getFhirVersionString())) {
serverFhirVersionEnum = FhirVersionEnum.DSTU3; serverFhirVersionEnum = FhirVersionEnum.DSTU3;
} else if (serverFhirVersionString.equals(FhirVersionEnum.R4.getFhirVersionString())) {
serverFhirVersionEnum = FhirVersionEnum.R4;
} else { } else {
// we'll be lenient and accept this // we'll be lenient and accept this
ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString); ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString);

View File

@ -1346,7 +1346,7 @@ public class VersionConvertor_14_40 {
org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent tgt = new org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent(); org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent tgt = new org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent();
copyElement(src, tgt); copyElement(src, tgt);
for (org.hl7.fhir.dstu2016may.model.StringType t : src.getDiscriminator()) for (org.hl7.fhir.dstu2016may.model.StringType t : src.getDiscriminator())
tgt.addDiscriminator(ProfileUtilities.interpretR2Discriminator(t.getValue())); tgt.addDiscriminator(ProfileUtilities.interpretR2Discriminator(t.getValue(), true));
if (src.hasDescription()) if (src.hasDescription())
tgt.setDescription(src.getDescription()); tgt.setDescription(src.getDescription());
if (src.hasOrdered()) if (src.hasOrdered())

View File

@ -1,83 +0,0 @@
package org.hl7.fhir.utilities;
/*
* #%L
* HAPI FHIR - Converter
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class OIDUtils {
/*
2.16.840.1.113883.3.72.5.2 - NIST owns this
2.16.840.1.113883.4.6 - National Provider Identifier
2.16.840.1.113883.6.21 - UB92
2.16.840.1.113883.6.69 - NDC
*/
public static String getUriForOid(String r) {
if (r.equals("2.16.840.1.113883.6.96"))
return "http://snomed.info/sct";
if (r.equals("2.16.840.1.113883.6.1"))
return "http://loinc.org";
if (r.equals("2.16.840.1.113883.6.8"))
return "http://unitsofmeasure.org";
if (r.equals("2.16.840.1.113883.6.3"))
return "http://hl7.org/fhir/sid/icd-10";
if (r.equals("2.16.840.1.113883.6.42"))
return "http://hl7.org/fhir/sid/icd-9";
if (r.equals("2.16.840.1.113883.6.104"))
return "http://hl7.org/fhir/sid/icd-9";
if (r.equals("2.16.840.1.113883.6.103"))
return "http://hl7.org/fhir/sid/icd-9"; //todo: confirm this
if (r.equals("2.16.840.1.113883.6.73"))
return "http://hl7.org/fhir/sid/atc";
if (r.equals("2.16.840.1.113883.3.26.1.1"))
return "http://ncimeta.nci.nih.gov";
if (r.equals("2.16.840.1.113883.3.26.1.1.1"))
return "http://ncimeta.nci.nih.gov";
if (r.equals("2.16.840.1.113883.6.88"))
return "http://www.nlm.nih.gov/research/umls/rxnorm"; // todo: confirm this
if (r.equals("2.16.840.1.113883.5.1008"))
return "http://hl7.org/fhir/v3/NullFlavor";
if (r.equals("2.16.840.1.113883.5.111"))
return "http://hl7.org/fhir/v3/RoleCode";
if (r.equals("2.16.840.1.113883.5.4"))
return "http://hl7.org/fhir/v3/ActCode";
if (r.equals("2.16.840.1.113883.5.8"))
return "http://hl7.org/fhir/v3/ActReason";
if (r.equals("2.16.840.1.113883.5.83"))
return "http://hl7.org/fhir/v3/ObservationInterpretation";
if (r.equals("2.16.840.1.113883.6.238"))
return "http://hl7.org/fhir/v3/Race";
if (r.equals("2.16.840.1.113883.6.59"))
return "http://hl7.org/fhir/sid/cvx";
if (r.equals("2.16.840.1.113883.12.292"))
return "http://hl7.org/fhir/sid/cvx";
if (r.equals("2.16.840.1.113883.6.12"))
return "http://www.ama-assn.org/go/cpt";
if (r.startsWith("2.16.840.1.113883.12."))
return "http://hl7.org/fhir/sid/v2-"+r.substring(21);
return null;
}
}

View File

@ -1,193 +0,0 @@
package org.hl7.fhir.utilities.ucum;
/*
* #%L
* HAPI FHIR - Converter
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
/*******************************************************************************
* Crown Copyright (c) 2006+, Copyright (c) 2006 - 2014 Kestral Computing & Health Intersections.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Kestral Computing P/L - initial implementation
* Health Intersections - ongoing maintenance
*******************************************************************************/
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.hl7.fhir.exceptions.UcumException;
/**
* General Ucum Service
*
* UCUM functionality that is useful for applications that make use of UCUM codes
*
* This is a tightly bound interface - consumers use the internal model classes
*
* @author Grahame Grieve
*
*/
public interface UcumService {
public class UcumVersionDetails {
private Date releaseDate;
private String version;
/**
* @param releaseDate
* @param version
*/
public UcumVersionDetails(Date releaseDate, String version) {
super();
this.releaseDate = releaseDate;
this.version = version;
}
/**
* @return the releaseDate
*/
public Date getReleaseDate() {
return releaseDate;
}
/**
* @return the version
*/
public String getVersion() {
return version;
}
}
/**
* return Ucum Identification details for the version in use
*/
public abstract UcumVersionDetails ucumIdentification();
/**
* Check UCUM. Note that this stands as a test of the service
* more than UCUM itself (for version 1.7, there are no known
* semantic errors in UCUM). But you should always run this test at least
* once with the version of UCUM you are using to ensure that
* the service implementation correctly understands the UCUM data
* to which it is bound
*
* @return a list of internal errors in the UCUM spec.
*
*/
public abstract List<String> validateUCUM();
/**
* return a list of the defined types of units in this UCUM version
*
* @return
*/
public abstract Set<String> getProperties();
/**
* validate whether a unit code are valid UCUM units
*
* @param units - the unit code to check
* @return nil if valid, or an error message describing the problem
*/
public abstract String validate(String unit);
/**
* given a unit, return a formal description of what the units stand for using
* full names
* @param units the unit code
* @return formal description
* @throws UcumException
* @throws OHFException
*/
public String analyse(String unit) throws UcumException ;
/**
* validate whether a units are valid UCUM units and additionally require that the
* units from a particular property
*
* @param units - the unit code to check
* @return nil if valid, or an error message describing the problem
*/
public abstract String validateInProperty(String unit, String property);
/**
* validate whether a units are valid UCUM units and additionally require that the
* units match a particular base canonical unit
*
* @param units - the unit code to check
* @return nil if valid, or an error message describing the problem
*/
public abstract String validateCanonicalUnits(String unit, String canonical);
/**
* given a set of units, return their canonical form
* @param unit
* @return the canonical form
* @throws UcumException
* @throws OHFException
*/
public abstract String getCanonicalUnits(String unit) throws UcumException ;
/**
* given two pairs of units, return true if they sahre the same canonical base
*
* @param units1
* @param units2
* @return
* @throws UcumException
* @
*/
public abstract boolean isComparable(String units1, String units2) throws UcumException ;
/**
* given a value and source unit, return the value in the given dest unit
* an exception is thrown if the conversion is not possible
*
* @param value
* @param sourceUnit
* @param destUnit
* @return the value if a conversion is possible
* @throws UcumException
* @throws OHFException
*/
public abstract Decimal convert(Decimal value, String sourceUnit, String destUnit) throws UcumException ;
/**
* given a set of UCUM units, return a likely preferred human dense form
*
* SI units - as is.
* Other units - improved by manual fixes, or the removal of []
*
* @param code
* @return the preferred human display form
*/
public abstract String getCommonDisplay(String code);
}

View File

@ -1,8 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<!-- The parent of this project is the deployable POM. This project isn't deployable, but this keeps it before the root pom in the reactor order when building the site. I don't know why this works... <!--
Need to investigate this. --> The parent of this project is the deployable POM. This project isn't
deployable, but this keeps it before the root pom in the reactor
order when building the site. I don't know why this works...
Need to investigate this.
-->
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
@ -139,7 +144,7 @@
<artifactId>jetty-util</artifactId> <artifactId>jetty-util</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.sf.json-lib</groupId> <groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId> <artifactId>json-lib</artifactId>
@ -225,14 +230,14 @@
<artifactId>spring-web</artifactId> <artifactId>spring-web</artifactId>
</dependency> </dependency>
<!-- <!--
For some reason JavaDoc crashed during site generation unless we have this dependency For some reason JavaDoc crashed during site generation unless we have this dependency
--> -->
<dependency> <dependency>
<groupId>javax.interceptor</groupId> <groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId> <artifactId>javax.interceptor-api</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@ -253,6 +258,13 @@
</plugins> </plugins>
</pluginManagement> </pluginManagement>
<plugins> <plugins>
<plugin>
<groupId>org.basepom.maven</groupId>
<artifactId>duplicate-finder-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <artifactId>jacoco-maven-plugin</artifactId>
@ -285,19 +297,17 @@
</fileSets> </fileSets>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
<id>post-integration-test</id> <id>post-integration-test</id>
<phase>install</phase> <phase>install</phase>
<goals> <goals>
<goal>report</goal> <goal>report</goal>
</goals> </goals>
<configuration> <configuration>
<dataFile>${project.build.directory}/jacoco.exec</dataFile> <dataFile>${project.build.directory}/jacoco.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-report</outputDirectory> <outputDirectory>${project.reporting.outputDirectory}/jacoco-report</outputDirectory>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin> <plugin>

View File

@ -74,6 +74,11 @@
</dependency> </dependency>
<!-- Unit test dependencies --> <!-- Unit test dependencies -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>

View File

@ -34,13 +34,15 @@ import ca.uhn.fhir.rest.client.impl.RestfulClientFactory;
/** /**
* A Restful Client Factory, based on Jax Rs * A Restful Client Factory, based on Jax Rs
* * Default Jax-Rs client is NOT thread safe in static context, you should create a new factory every time or
* use a specific Jax-Rs client implementation which managed connection pool.
* @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
*/ */
public class JaxRsRestfulClientFactory extends RestfulClientFactory { public class JaxRsRestfulClientFactory extends RestfulClientFactory {
private Client myNativeClient; private Client myNativeClient;
private List<Class<?>> registeredComponents;
/** /**
* Constructor. Note that you must set the {@link FhirContext} manually using {@link #setFhirContext(FhirContext)} if this constructor is used! * Constructor. Note that you must set the {@link FhirContext} manually using {@link #setFhirContext(FhirContext)} if this constructor is used!
*/ */
@ -64,6 +66,12 @@ public class JaxRsRestfulClientFactory extends RestfulClientFactory {
myNativeClient = builder.build(); myNativeClient = builder.build();
} }
if (registeredComponents != null && !registeredComponents.isEmpty()) {
for (Class<?> c : registeredComponents) {
myNativeClient = myNativeClient.register(c);
}
}
return myNativeClient; return myNativeClient;
} }
@ -73,29 +81,46 @@ public class JaxRsRestfulClientFactory extends RestfulClientFactory {
return new JaxRsHttpClient(client, url, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders); return new JaxRsHttpClient(client, url, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders);
} }
/***
* Not supported with default Jax-Rs client implementation
* @param theHost
* The host (or null to disable proxying, as is the default)
* @param thePort
*/
@Override @Override
public void setProxy(String theHost, Integer thePort) { public void setProxy(String theHost, Integer thePort) {
throw new UnsupportedOperationException("Proxies are not supported yet in JAX-RS client"); throw new UnsupportedOperationException("Proxies are not supported yet in JAX-RS client");
} }
/** /**
* Only accept clients of type javax.ws.rs.client.Client * Only accept clients of type javax.ws.rs.client.Client
* * Can be used to set a specific Client implementation
* @param theHttpClient * @param theHttpClient
*/ */
@Override @Override
public synchronized void setHttpClient(Object theHttpClient) { public synchronized void setHttpClient(Object theHttpClient) {
this.myNativeClient = (Client) theHttpClient; this.myNativeClient = (Client) theHttpClient;
} }
/**
* Register a list of Jax-Rs component (provider, filter...)
* @param components list of Jax-Rs components to register
*/
public void register(List<Class<?>> components) {
registeredComponents = components;
}
@Override @Override
protected JaxRsHttpClient getHttpClient(String theServerBase) { protected JaxRsHttpClient getHttpClient(String theServerBase) {
return new JaxRsHttpClient(getNativeClientClient(), new StringBuilder(theServerBase), null, null, null, null); return new JaxRsHttpClient(getNativeClientClient(), new StringBuilder(theServerBase), null, null, null, null);
} }
@Override @Override
protected void resetHttpClient() { protected void resetHttpClient() {
this.myNativeClient = null; if (myNativeClient != null)
} myNativeClient.close(); // close client to avoid memory leak
myNativeClient = null;
}
} }

View File

@ -0,0 +1,53 @@
package ca.uhn.fhir.jaxrs.client;
import ca.uhn.fhir.context.FhirContext;
import org.junit.Before;
import org.junit.Test;
import javax.ws.rs.client.Client;
import java.util.ArrayList;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Created by Sebastien Riviere on 31/07/2017.
*/
public class JaxRsRestfulClientFactoryTest {
private final FhirContext context = FhirContext.forDstu2();
private JaxRsRestfulClientFactory factory;
@Before
public void setUp() {
factory = new JaxRsRestfulClientFactory(context);
}
@Test
public void emptyConstructorTest() {
assertNotNull(new JaxRsRestfulClientFactory());
}
@Test
public void getDefaultNativeClientTest() {
assertNotNull(factory.getNativeClientClient());
}
@Test
public void getNativeClientEmptyRegisteredComponentListTest() {
factory.register(new ArrayList<Class<?>>());
final Client result = factory.getNativeClientClient();
assertNotNull(result);
assertTrue(result.getConfiguration().getClasses().isEmpty());
}
@Test
public void getNativeClientRegisteredComponentListTest() {
factory.register(Arrays.asList(MyFilter.class, String.class));
final Client result = factory.getNativeClientClient();
assertNotNull(result);
assertEquals(1, result.getConfiguration().getClasses().size());
}
}

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.jaxrs.client;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
/**
* Created by Sebastien Riviere on 31/07/2017.
*/
@Provider
public class MyFilter implements ClientResponseFilter {
@Override
public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext) throws IOException {
}
}

View File

@ -1,4 +1,4 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
@ -32,6 +32,12 @@
<version>3.5</version> <version>3.5</version>
</dependency> </dependency>
--> -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
</dependency>
<dependency> <dependency>
<groupId>net.sf.saxon</groupId> <groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId> <artifactId>Saxon-HE</artifactId>
@ -105,10 +111,12 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId> <artifactId>jcl-over-slf4j</artifactId>
</dependency> </dependency>
-->
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
@ -163,7 +171,7 @@
<artifactId>thymeleaf-spring4</artifactId> <artifactId>thymeleaf-spring4</artifactId>
</dependency> </dependency>
<!-- For UCUM --> <!-- For UCUM: TODO we should replace this with org.fhir UCUM -->
<dependency> <dependency>
<groupId>org.jscience</groupId> <groupId>org.jscience</groupId>
<artifactId>jscience</artifactId> <artifactId>jscience</artifactId>
@ -233,10 +241,12 @@
<!-- <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.2</version> </dependency> --> <!-- <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.2</version> </dependency> -->
<!-- Spring --> <!-- Spring -->
<!--
<dependency> <dependency>
<groupId>aopalliance</groupId> <groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId> <artifactId>aopalliance</artifactId>
</dependency> </dependency>
-->
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId> <artifactId>spring-core</artifactId>
@ -358,6 +368,7 @@
<dependency> <dependency>
<groupId>javax.mail</groupId> <groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId> <artifactId>javax.mail-api</artifactId>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.mail</groupId> <groupId>com.sun.mail</groupId>

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.search.*; import ca.uhn.fhir.jpa.search.*;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl; import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor; import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
@ -93,15 +94,6 @@ public class BaseConfig implements SchedulingConfigurer {
return new StaleSearchDeletingSvcImpl(); return new StaleSearchDeletingSvcImpl();
} }
// @PostConstruct
// public void wireResourceDaos() {
// Map<String, IDao> daoBeans = myAppCtx.getBeansOfType(IDao.class);
// List bean = myAppCtx.getBean("myResourceProvidersDstu2", List.class);
// for (IDao next : daoBeans.values()) {
// next.setResourceDaos(bean);
// }
// }
@Bean @Bean
@Lazy @Lazy
public SubscriptionRestHookInterceptor subscriptionRestHookInterceptor() { public SubscriptionRestHookInterceptor subscriptionRestHookInterceptor() {
@ -114,15 +106,23 @@ public class BaseConfig implements SchedulingConfigurer {
return new SubscriptionWebsocketInterceptor(); return new SubscriptionWebsocketInterceptor();
} }
/**
* Note: If you're going to use this, you need to provide a bean
* of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender}
* in your own Spring config
*/
@Bean
@Lazy
public SubscriptionEmailInterceptor subscriptionEmailInterceptor() {
return new SubscriptionEmailInterceptor();
}
@Bean @Bean
public TaskScheduler taskScheduler() { public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler(); ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService().getObject()); retVal.setConcurrentExecutor(scheduledExecutorService().getObject());
retVal.setScheduledExecutor(scheduledExecutorService().getObject()); retVal.setScheduledExecutor(scheduledExecutorService().getObject());
return retVal; return retVal;
// ThreadPoolTaskScheduler retVal = new ThreadPoolTaskScheduler();
// retVal.setPoolSize(5);
// return retVal;
} }
/** /**

View File

@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
@ -51,10 +50,7 @@ import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.*;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -2044,7 +2040,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
if (theResource instanceof IResource) { if (theResource instanceof IResource) {
IResource resource = (IResource) theResource; IResource resource = (IResource) theResource;
List<XMLEvent> xmlEvents = resource.getText().getDiv().getValue(); List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
if (xmlEvents != null) { if (xmlEvents != null) {
for (XMLEvent next : xmlEvents) { for (XMLEvent next : xmlEvents) {
if (next.isCharacters()) { if (next.isCharacters()) {
@ -2057,8 +2053,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
IDomainResource resource = (IDomainResource) theResource; IDomainResource resource = (IDomainResource) theResource;
try { try {
String divAsString = resource.getText().getDivAsString(); String divAsString = resource.getText().getDivAsString();
XhtmlDt xhtml = new XhtmlDt(divAsString); List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
List<XMLEvent> xmlEvents = xhtml.getValue();
if (xmlEvents != null) { if (xmlEvents != null) {
for (XMLEvent next : xmlEvents) { for (XMLEvent next : xmlEvents) {
if (next.isCharacters()) { if (next.isCharacters()) {

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils; import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
@ -61,6 +62,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletResponse;
import java.util.*; import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -894,6 +896,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Transactional(propagation = Propagation.SUPPORTS) @Transactional(propagation = Propagation.SUPPORTS)
@Override @Override
public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails) { public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails) {
return search(theParams, theRequestDetails, null);
}
@Transactional(propagation = Propagation.SUPPORTS)
@Override
public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
for (List<List<? extends IQueryParameterType>> nextAnds : theParams.values()) { for (List<List<? extends IQueryParameterType>> nextAnds : theParams.values()) {
@ -930,7 +938,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
cacheControlDirective.parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)); cacheControlDirective.parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL));
} }
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective); IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective);
if (retVal instanceof PersistedJpaBundleProvider) {
PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
if (provider.isCacheHit()) {
if (theServletResponse != null && theRequestDetails != null) {
String value = "HIT from " + theRequestDetails.getFhirServerBase();
theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
}
}
}
return retVal;
} }
@Override @Override

View File

@ -108,6 +108,7 @@ public class DaoConfig {
private Set<String> myTreatReferencesAsLogical = new HashSet<String>(DEFAULT_LOGICAL_BASE_URLS); private Set<String> myTreatReferencesAsLogical = new HashSet<String>(DEFAULT_LOGICAL_BASE_URLS);
private boolean myAutoCreatePlaceholderReferenceTargets; private boolean myAutoCreatePlaceholderReferenceTargets;
private Integer myCacheControlNoStoreMaxResultsUpperLimit = 1000; private Integer myCacheControlNoStoreMaxResultsUpperLimit = 1000;
private Integer myCountSearchResultsUpTo = null;
/** /**
* Constructor * Constructor
@ -152,6 +153,56 @@ public class DaoConfig {
myCacheControlNoStoreMaxResultsUpperLimit = theCacheControlNoStoreMaxResults; myCacheControlNoStoreMaxResultsUpperLimit = theCacheControlNoStoreMaxResults;
} }
/**
* When searching, if set to a non-null value (default is <code>null</code>) the
* search coordinator will attempt to find at least this many results
* before returning a response to the client. This parameter mainly affects
* whether a "total count" is included in the response bundle for searches that
* return large amounts of data.
* <p>
* For a search that returns 10000 results, if this value is set to
* 10000 the search coordinator will find all 10000 results
* prior to returning, so the initial response bundle will have the
* total set to 10000. If this value is null (or less than 10000)
* the response bundle will likely return slightly faster, but will
* not include the total. Subsequent page requests will likely
* include the total however, if they are performed after the
* search coordinator has found all results.
* </p>
* <p>
* Set this value to <code>0</code> to always load all
* results before returning.
* </p>
*/
public Integer getCountSearchResultsUpTo() {
return myCountSearchResultsUpTo;
}
/**
* When searching, if set to a non-null value (default is <code>null</code>) the
* search coordinator will attempt to find at least this many results
* before returning a response to the client. This parameter mainly affects
* whether a "total count" is included in the response bundle for searches that
* return large amounts of data.
* <p>
* For a search that returns 10000 results, if this value is set to
* 10000 the search coordinator will find all 10000 results
* prior to returning, so the initial response bundle will have the
* total set to 10000. If this value is null (or less than 10000)
* the response bundle will likely return slightly faster, but will
* not include the total. Subsequent page requests will likely
* include the total however, if they are performed after the
* search coordinator has found all results.
* </p>
* <p>
* Set this value to <code>0</code> to always load all
* results before returning.
* </p>
*/
public void setCountSearchResultsUpTo(Integer theCountSearchResultsUpTo) {
myCountSearchResultsUpTo = theCountSearchResultsUpTo;
}
/** /**
* When a code system is added that contains more than this number of codes, * When a code system is added that contains more than this number of codes,
* the code system will be indexed later in an incremental process in order to * the code system will be indexed later in an incremental process in order to
@ -357,11 +408,8 @@ public class DaoConfig {
/** /**
* This may be used to optionally register server interceptors directly against the DAOs. * This may be used to optionally register server interceptors directly against the DAOs.
*/ */
public void setInterceptors(IServerInterceptor... theInterceptor) { public void setInterceptors(List<IServerInterceptor> theInterceptors) {
setInterceptors(new ArrayList<IServerInterceptor>()); myInterceptors = theInterceptors;
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
} }
/** /**
@ -959,8 +1007,11 @@ public class DaoConfig {
/** /**
* This may be used to optionally register server interceptors directly against the DAOs. * This may be used to optionally register server interceptors directly against the DAOs.
*/ */
public void setInterceptors(List<IServerInterceptor> theInterceptors) { public void setInterceptors(IServerInterceptor... theInterceptor) {
myInterceptors = theInterceptors; setInterceptors(new ArrayList<IServerInterceptor>());
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
} }
/** /**

View File

@ -33,6 +33,10 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
public interface IFhirResourceDao<T extends IBaseResource> extends IDao { public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
@ -179,6 +183,9 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
IBundleProvider search(SearchParameterMap theParams, RequestDetails theRequestDetails); IBundleProvider search(SearchParameterMap theParams, RequestDetails theRequestDetails);
@Transactional(propagation = Propagation.SUPPORTS)
IBundleProvider search(SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse);
Set<Long> searchForIds(SearchParameterMap theParams); Set<Long> searchForIds(SearchParameterMap theParams);
/** /**

View File

@ -274,14 +274,6 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
return this; return this;
} }
/**
* @deprecated As of HAPI FHIR 2.4 this method no longer does anything
*/
@Deprecated
public void setPersistResults(boolean thePersistResults) {
// does nothing as of HAPI FHIR 2.4
}
public void setRevIncludes(Set<Include> theRevIncludes) { public void setRevIncludes(Set<Include> theRevIncludes) {
myRevIncludes = theRevIncludes; myRevIncludes = theRevIncludes;
} }

View File

@ -29,6 +29,7 @@ import java.util.Map.Entry;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.rest.param.ParameterUtil;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.hibernate.Session; import org.hibernate.Session;
@ -438,7 +439,11 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
DaoMethodOutcome outcome; DaoMethodOutcome outcome;
UrlParts parts = UrlUtil.parseUrl(url); UrlParts parts = UrlUtil.parseUrl(url);
if (isNotBlank(parts.getResourceId())) { if (isNotBlank(parts.getResourceId())) {
res.setId(new IdType(parts.getResourceType(), parts.getResourceId())); String version = null;
if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
version = ParameterUtil.parseETagValue(nextReqEntry.getRequest().getIfMatch());
}
res.setId(new IdType(parts.getResourceType(), parts.getResourceId(), version));
outcome = resourceDao.update(res, null, false, theRequestDetails); outcome = resourceDao.update(res, null, false, theRequestDetails);
} else { } else {
res.setId((String) null); res.setId((String) null);

View File

@ -215,6 +215,9 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry {
case RETIRED: case RETIRED:
status = RuntimeSearchParamStatusEnum.RETIRED; status = RuntimeSearchParamStatusEnum.RETIRED;
break; break;
case UNKNOWN:
status = RuntimeSearchParamStatusEnum.UNKNOWN;
break;
case NULL: case NULL:
break; break;
} }

View File

@ -28,6 +28,7 @@ import java.util.Map.Entry;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import ca.uhn.fhir.rest.param.ParameterUtil;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
@ -435,7 +436,11 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
DaoMethodOutcome outcome; DaoMethodOutcome outcome;
UrlParts parts = UrlUtil.parseUrl(url); UrlParts parts = UrlUtil.parseUrl(url);
if (isNotBlank(parts.getResourceId())) { if (isNotBlank(parts.getResourceId())) {
res.setId(new IdType(parts.getResourceType(), parts.getResourceId())); String version = null;
if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
version = ParameterUtil.parseETagValue(nextReqEntry.getRequest().getIfMatch());
}
res.setId(new IdType(parts.getResourceType(), parts.getResourceId(), version));
outcome = resourceDao.update(res, null, false, theRequestDetails); outcome = resourceDao.update(res, null, false, theRequestDetails);
} else { } else {
res.setId((String) null); res.setId((String) null);

View File

@ -216,6 +216,9 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry {
case RETIRED: case RETIRED:
status = RuntimeSearchParamStatusEnum.RETIRED; status = RuntimeSearchParamStatusEnum.RETIRED;
break; break;
case UNKNOWN:
status = RuntimeSearchParamStatusEnum.UNKNOWN;
break;
case NULL: case NULL:
break; break;
} }

View File

@ -115,6 +115,8 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
break; break;
case URI: case URI:
break; break;
case HAS:
break;
} }
params.add(nextArgument.getName(), param); params.add(nextArgument.getName(), param);

View File

@ -24,6 +24,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -47,7 +48,7 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus<Bundl
@Qualifier("mySystemDaoDstu2") @Qualifier("mySystemDaoDstu2")
private IFhirSystemDao<Bundle, MetaDt> mySystemDao; private IFhirSystemDao<Bundle, MetaDt> mySystemDao;
@Autowired @Autowired(required = false)
private IFulltextSearchSvc mySearchDao; private IFulltextSearchSvc mySearchDao;
//@formatter:off //@formatter:off
@ -178,7 +179,8 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus<Bundl
@OperationParam(name="searchParam", min=1, max=1) String theSearchParam, @OperationParam(name="searchParam", min=1, max=1) String theSearchParam,
@OperationParam(name="text", min=1, max=1) String theText @OperationParam(name="text", min=1, max=1) String theText
) { ) {
JpaSystemProviderDstu3.validateFulltextSearchEnabled(mySearchDao);
if (isBlank(theContext)) { if (isBlank(theContext)) {
throw new InvalidRequestException("Parameter 'context' must be provided"); throw new InvalidRequestException("Parameter 'context' must be provided");
} }
@ -188,16 +190,14 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus<Bundl
if (isBlank(theText)) { if (isBlank(theText)) {
throw new InvalidRequestException("Parameter 'text' must be provided"); throw new InvalidRequestException("Parameter 'text' must be provided");
} }
List<Suggestion> keywords = mySearchDao.suggestKeywords(theContext, theSearchParam, theText); List<Suggestion> keywords = mySearchDao.suggestKeywords(theContext, theSearchParam, theText);
Parameters retVal = new Parameters(); Parameters retVal = new Parameters();
for (Suggestion next : keywords) { for (Suggestion next : keywords) {
//@formatter:off
retVal.addParameter() retVal.addParameter()
.addPart(new Parameter().setName("keyword").setValue(new StringDt(next.getTerm()))) .addPart(new Parameter().setName("keyword").setValue(new StringDt(next.getTerm())))
.addPart(new Parameter().setName("score").setValue(new DecimalDt(next.getScore()))); .addPart(new Parameter().setName("score").setValue(new DecimalDt(next.getScore())));
//@formatter:on
} }
return retVal; return retVal;

View File

@ -1,5 +1,27 @@
package ca.uhn.fhir.jpa.provider.dstu3; package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
/* /*
@ -21,23 +43,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import java.util.*;
import java.util.Map.Entry;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundle, Meta> { public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundle, Meta> {
@ -45,122 +50,122 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
@Qualifier("mySystemDaoDstu3") @Qualifier("mySystemDaoDstu3")
private IFhirSystemDao<Bundle, Meta> mySystemDao; private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired @Autowired(required = false)
private IFulltextSearchSvc mySearchDao; private IFulltextSearchSvc mySearchDao;
//@formatter:off //@formatter:off
// This is generated by hand: // This is generated by hand:
// ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerType.class, min=0, max=1),/" // ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerType.class, min=0, max=1),/"
@Operation(name="$get-resource-counts", idempotent=true, returnParameters= { @Operation(name = "$get-resource-counts", idempotent = true, returnParameters = {
@OperationParam(name="AllergyIntolerance", type=IntegerType.class, min=0, max=1), @OperationParam(name = "AllergyIntolerance", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Appointment", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Appointment", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="AppointmentResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "AppointmentResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="AuditEvent", type=IntegerType.class, min=0, max=1), @OperationParam(name = "AuditEvent", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Basic", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Basic", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Binary", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Binary", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="BodySite", type=IntegerType.class, min=0, max=1), @OperationParam(name = "BodySite", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Bundle", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Bundle", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="CarePlan", type=IntegerType.class, min=0, max=1), @OperationParam(name = "CarePlan", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="CarePlan2", type=IntegerType.class, min=0, max=1), @OperationParam(name = "CarePlan2", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Claim", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Claim", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ClaimResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ClaimResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ClinicalImpression", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ClinicalImpression", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Communication", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Communication", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="CommunicationRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "CommunicationRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Composition", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Composition", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ConceptMap", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ConceptMap", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Condition", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Condition", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Conformance", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Conformance", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Contract", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Contract", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Contraindication", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Contraindication", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Coverage", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Coverage", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DataElement", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DataElement", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Device", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Device", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DeviceComponent", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DeviceComponent", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DeviceMetric", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DeviceMetric", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DeviceUseRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DeviceUseRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DeviceUseStatement", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DeviceUseStatement", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DiagnosticOrder", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DiagnosticOrder", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DiagnosticReport", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DiagnosticReport", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DocumentManifest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DocumentManifest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="DocumentReference", type=IntegerType.class, min=0, max=1), @OperationParam(name = "DocumentReference", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="EligibilityRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "EligibilityRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="EligibilityResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "EligibilityResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Encounter", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Encounter", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="EnrollmentRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "EnrollmentRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="EnrollmentResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "EnrollmentResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="EpisodeOfCare", type=IntegerType.class, min=0, max=1), @OperationParam(name = "EpisodeOfCare", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ExplanationOfBenefit", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ExplanationOfBenefit", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="FamilyMemberHistory", type=IntegerType.class, min=0, max=1), @OperationParam(name = "FamilyMemberHistory", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Flag", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Flag", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Goal", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Goal", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Group", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Group", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="HealthcareService", type=IntegerType.class, min=0, max=1), @OperationParam(name = "HealthcareService", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ImagingObjectSelection", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ImagingObjectSelection", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ImagingStudy", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ImagingStudy", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Immunization", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Immunization", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ImmunizationRecommendation", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ImmunizationRecommendation", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ListResource", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ListResource", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Location", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Location", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Media", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Media", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Medication", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Medication", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="MedicationAdministration", type=IntegerType.class, min=0, max=1), @OperationParam(name = "MedicationAdministration", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="MedicationDispense", type=IntegerType.class, min=0, max=1), @OperationParam(name = "MedicationDispense", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="MedicationPrescription", type=IntegerType.class, min=0, max=1), @OperationParam(name = "MedicationPrescription", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="MedicationStatement", type=IntegerType.class, min=0, max=1), @OperationParam(name = "MedicationStatement", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="MessageHeader", type=IntegerType.class, min=0, max=1), @OperationParam(name = "MessageHeader", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="NamingSystem", type=IntegerType.class, min=0, max=1), @OperationParam(name = "NamingSystem", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="NutritionOrder", type=IntegerType.class, min=0, max=1), @OperationParam(name = "NutritionOrder", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Observation", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Observation", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="OperationDefinition", type=IntegerType.class, min=0, max=1), @OperationParam(name = "OperationDefinition", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="OperationOutcome", type=IntegerType.class, min=0, max=1), @OperationParam(name = "OperationOutcome", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Order", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Order", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="OrderResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "OrderResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Organization", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Organization", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Parameters", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Parameters", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Patient", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Patient", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="PaymentNotice", type=IntegerType.class, min=0, max=1), @OperationParam(name = "PaymentNotice", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="PaymentReconciliation", type=IntegerType.class, min=0, max=1), @OperationParam(name = "PaymentReconciliation", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Person", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Person", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Practitioner", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Practitioner", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Procedure", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Procedure", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ProcedureRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ProcedureRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ProcessRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ProcessRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ProcessResponse", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ProcessResponse", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Provenance", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Provenance", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Questionnaire", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Questionnaire", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="QuestionnaireAnswers", type=IntegerType.class, min=0, max=1), @OperationParam(name = "QuestionnaireAnswers", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ReferralRequest", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ReferralRequest", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="RelatedPerson", type=IntegerType.class, min=0, max=1), @OperationParam(name = "RelatedPerson", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="RiskAssessment", type=IntegerType.class, min=0, max=1), @OperationParam(name = "RiskAssessment", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Schedule", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Schedule", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="SearchParameter", type=IntegerType.class, min=0, max=1), @OperationParam(name = "SearchParameter", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Slot", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Slot", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Specimen", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Specimen", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="StructureDefinition", type=IntegerType.class, min=0, max=1), @OperationParam(name = "StructureDefinition", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Subscription", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Subscription", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Substance", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Substance", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="Supply", type=IntegerType.class, min=0, max=1), @OperationParam(name = "Supply", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="ValueSet", type=IntegerType.class, min=0, max=1), @OperationParam(name = "ValueSet", type = IntegerType.class, min = 0, max = 1),
@OperationParam(name="VisionPrescription", type=IntegerType.class, min=0, max=1) @OperationParam(name = "VisionPrescription", type = IntegerType.class, min = 0, max = 1)
}) })
@Description(shortDefinition="Provides the number of resources currently stored on the server, broken down by resource type") @Description(shortDefinition = "Provides the number of resources currently stored on the server, broken down by resource type")
//@formatter:on //@formatter:on
public Parameters getResourceCounts() { public Parameters getResourceCounts() {
Parameters retVal = new Parameters(); Parameters retVal = new Parameters();
Map<String, Long> counts = mySystemDao.getResourceCounts(); Map<String, Long> counts = mySystemDao.getResourceCounts();
counts = new TreeMap<String, Long>(counts); counts = new TreeMap<String, Long>(counts);
for (Entry<String, Long> nextEntry : counts.entrySet()) { for (Entry<String, Long> nextEntry : counts.entrySet()) {
retVal.addParameter().setName((nextEntry.getKey())).setValue(new IntegerType(nextEntry.getValue().intValue())); retVal.addParameter().setName((nextEntry.getKey())).setValue(new IntegerType(nextEntry.getValue().intValue()));
} }
return retVal; return retVal;
} }
//@formatter:off //@formatter:off
@Operation(name="$meta", idempotent=true, returnParameters= { @Operation(name = "$meta", idempotent = true, returnParameters = {
@OperationParam(name="return", type=Meta.class) @OperationParam(name = "return", type = Meta.class)
}) })
//@formatter:on //@formatter:on
public Parameters meta(RequestDetails theRequestDetails) { public Parameters meta(RequestDetails theRequestDetails) {
@ -168,14 +173,15 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
parameters.addParameter().setName("return").setValue(getDao().metaGetOperation(theRequestDetails)); parameters.addParameter().setName("return").setValue(getDao().metaGetOperation(theRequestDetails));
return parameters; return parameters;
} }
@Operation(name="$suggest-keywords", idempotent=true) @Operation(name = "$suggest-keywords", idempotent = true)
public Parameters suggestKeywords( public Parameters suggestKeywords(
@OperationParam(name="context", min=1, max=1) String theContext, @OperationParam(name = "context", min = 1, max = 1) String theContext,
@OperationParam(name="searchParam", min=1, max=1) String theSearchParam, @OperationParam(name = "searchParam", min = 1, max = 1) String theSearchParam,
@OperationParam(name="text", min=1, max=1) String theText @OperationParam(name = "text", min = 1, max = 1) String theText
) { ) {
if (isBlank(theContext)) { if (isBlank(theContext)) {
throw new InvalidRequestException("Parameter 'context' must be provided"); throw new InvalidRequestException("Parameter 'context' must be provided");
} }
@ -185,21 +191,21 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
if (isBlank(theText)) { if (isBlank(theText)) {
throw new InvalidRequestException("Parameter 'text' must be provided"); throw new InvalidRequestException("Parameter 'text' must be provided");
} }
List<Suggestion> keywords = mySearchDao.suggestKeywords(theContext, theSearchParam, theText); List<Suggestion> keywords = mySearchDao.suggestKeywords(theContext, theSearchParam, theText);
Parameters retVal = new Parameters(); Parameters retVal = new Parameters();
for (Suggestion next : keywords) { for (Suggestion next : keywords) {
//@formatter:off //@formatter:off
retVal.addParameter() retVal.addParameter()
.addPart(new ParametersParameterComponent().setName("keyword").setValue(new StringType(next.getTerm()))) .addPart(new ParametersParameterComponent().setName("keyword").setValue(new StringType(next.getTerm())))
.addPart(new ParametersParameterComponent().setName("score").setValue(new DecimalType(next.getScore()))); .addPart(new ParametersParameterComponent().setName("score").setValue(new DecimalType(next.getScore())));
//@formatter:on //@formatter:on
} }
return retVal; return retVal;
} }
@Transaction @Transaction
public Bundle transaction(RequestDetails theRequestDetails, @TransactionParam Bundle theResources) { public Bundle transaction(RequestDetails theRequestDetails, @TransactionParam Bundle theResources) {
startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());
@ -210,4 +216,10 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
} }
} }
public static void validateFulltextSearchEnabled(IFulltextSearchSvc theSearchDao) {
if (theSearchDao == null || theSearchDao.isDisabled()) {
throw new InvalidRequestException("Fulltext searching is disabled on this server");
}
}
} }

View File

@ -45,7 +45,7 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus<Bundle,
@Qualifier("mySystemDaoR4") @Qualifier("mySystemDaoR4")
private IFhirSystemDao<Bundle, Meta> mySystemDao; private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired @Autowired(required = false)
private IFulltextSearchSvc mySearchDao; private IFulltextSearchSvc mySearchDao;
//@formatter:off //@formatter:off
@ -175,6 +175,7 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus<Bundle,
@OperationParam(name="searchParam", min=1, max=1) String theSearchParam, @OperationParam(name="searchParam", min=1, max=1) String theSearchParam,
@OperationParam(name="text", min=1, max=1) String theText @OperationParam(name="text", min=1, max=1) String theText
) { ) {
ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3.validateFulltextSearchEnabled(mySearchDao);
if (isBlank(theContext)) { if (isBlank(theContext)) {
throw new InvalidRequestException("Parameter 'context' must be provided"); throw new InvalidRequestException("Parameter 'context' must be provided");

View File

@ -19,23 +19,32 @@ package ca.uhn.fhir.jpa.search;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import java.util.*;
import javax.persistence.*;
import javax.persistence.criteria.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.*;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IDao; import ca.uhn.fhir.jpa.dao.IDao;
import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
public class PersistedJpaBundleProvider implements IBundleProvider { public class PersistedJpaBundleProvider implements IBundleProvider {
@ -47,6 +56,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
private ISearchDao mySearchDao; private ISearchDao mySearchDao;
private Search mySearchEntity; private Search mySearchEntity;
private String myUuid; private String myUuid;
private boolean myCacheHit;
public PersistedJpaBundleProvider(String theSearchUuid, IDao theDao) { public PersistedJpaBundleProvider(String theSearchUuid, IDao theDao) {
myUuid = theSearchUuid; myUuid = theSearchUuid;
@ -179,17 +189,17 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
}); });
switch (mySearchEntity.getSearchType()) { switch (mySearchEntity.getSearchType()) {
case HISTORY: case HISTORY:
return template.execute(new TransactionCallback<List<IBaseResource>>() { return template.execute(new TransactionCallback<List<IBaseResource>>() {
@Override @Override
public List<IBaseResource> doInTransaction(TransactionStatus theStatus) { public List<IBaseResource> doInTransaction(TransactionStatus theStatus) {
return doHistoryInTransaction(theFromIndex, theToIndex); return doHistoryInTransaction(theFromIndex, theToIndex);
} }
}); });
case SEARCH: case SEARCH:
case EVERYTHING: case EVERYTHING:
default: default:
return doSearchOrEverything(theFromIndex, theToIndex); return doSearchOrEverything(theFromIndex, theToIndex);
} }
} }
@ -197,6 +207,14 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
return myUuid; return myUuid;
} }
public boolean isCacheHit() {
return myCacheHit;
}
public void setCacheHit(boolean theCacheHit) {
myCacheHit = theCacheHit;
}
@Override @Override
public Integer preferredPageSize() { public Integer preferredPageSize() {
ensureSearchEntityLoaded(); ensureSearchEntityLoaded();

View File

@ -67,8 +67,11 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
@Override @Override
public Integer size() { public Integer size() {
mySearchTask.awaitInitialSync(); Integer size = mySearchTask.awaitInitialSync();
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch); SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
if (size != null) {
return size;
}
return super.size(); return super.size();
} }

View File

@ -17,7 +17,7 @@ package ca.uhn.fhir.jpa.search;
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
* #L% * #L%family
*/ */
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
@ -184,6 +184,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
final String searchUuid = UUID.randomUUID().toString(); final String searchUuid = UUID.randomUUID().toString();
ourLog.debug("Registering new search {}", searchUuid);
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass(); Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
final ISearchBuilder sb = theCallingDao.newSearchBuilder(); final ISearchBuilder sb = theCallingDao.newSearchBuilder();
sb.setType(resourceTypeClass, theResourceType); sb.setType(resourceTypeClass, theResourceType);
@ -204,6 +206,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) { if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) {
ourLog.debug("Search {} is loading in synchronous mode", searchUuid);
// Execute the query and make sure we return distinct results // Execute the query and make sure we return distinct results
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
@ -281,6 +285,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
mySearchDao.updateSearchLastReturned(searchToUse.getId(), new Date()); mySearchDao.updateSearchLastReturned(searchToUse.getId(), new Date());
retVal = new PersistedJpaBundleProvider(searchToUse.getUuid(), theCallingDao); retVal = new PersistedJpaBundleProvider(searchToUse.getUuid(), theCallingDao);
retVal.setCacheHit(true);
populateBundleProvider(retVal); populateBundleProvider(retVal);
} }
@ -437,7 +443,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
mySearchUuid = theSearchUuid; mySearchUuid = theSearchUuid;
} }
public void awaitInitialSync() { public Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync"); ourLog.trace("Awaiting initial sync");
do { do {
try { try {
@ -449,6 +455,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
} while (mySearch.getStatus() == SearchStatusEnum.LOADING); } while (mySearch.getStatus() == SearchStatusEnum.LOADING);
ourLog.trace("Initial sync completed"); ourLog.trace("Initial sync completed");
return mySearch.getTotalCount();
} }
@Override @Override
@ -507,6 +515,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
myIdToSearchTask.remove(mySearch.getUuid()); myIdToSearchTask.remove(mySearch.getUuid());
myInitialCollectionLatch.countDown();
myCompletionLatch.countDown(); myCompletionLatch.countDown();
return null; return null;
} }
@ -552,27 +561,27 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public List<Long> getResourcePids(int theFromIndex, int theToIndex) { public List<Long> getResourcePids(int theFromIndex, int theToIndex) {
ourLog.info("Requesting search PIDs from {}-{}", theFromIndex, theToIndex); ourLog.info("Requesting search PIDs from {}-{}", theFromIndex, theToIndex);
CountDownLatch latch = null; boolean keepWaiting;
synchronized (mySyncedPids) { do {
if (mySyncedPids.size() < theToIndex && mySearch.getStatus() == SearchStatusEnum.LOADING) { synchronized (mySyncedPids) {
int latchSize = theToIndex - mySyncedPids.size(); keepWaiting = false;
ourLog.trace("Registering latch to await {} results (want {} total)", latchSize, theToIndex); if (mySyncedPids.size() < theToIndex && mySearch.getStatus() == SearchStatusEnum.LOADING) {
latch = new CountDownLatch(latchSize); keepWaiting = true;
}
}
if (latch != null) {
while (latch.getCount() > 0 && mySearch.getStatus() == SearchStatusEnum.LOADING) {
try {
ourLog.trace("Awaiting latch with {}", latch.getCount());
latch.await(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// ok
} }
} }
} if (keepWaiting) {
ourLog.info("Waiting, as we only have {} results", mySyncedPids.size());
try {
Thread.sleep(500);
} catch (InterruptedException theE) {
// ignore
}
}
} while (keepWaiting);
ArrayList<Long> retVal = new ArrayList<Long>(); ourLog.info("Proceeding, as we have {} results", mySyncedPids.size());
ArrayList<Long> retVal = new ArrayList<>();
synchronized (mySyncedPids) { synchronized (mySyncedPids) {
verifySearchHasntFailedOrThrowInternalErrorException(mySearch); verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
@ -585,6 +594,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
} }
ourLog.info("Done syncing results", mySyncedPids.size());
return retVal; return retVal;
} }
@ -633,8 +644,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myUnsyncedPids.clear(); myUnsyncedPids.clear();
if (theResultIter.hasNext() == false) { if (theResultIter.hasNext() == false) {
mySearch.setStatus(SearchStatusEnum.FINISHED);
mySearch.setTotalCount(myCountSaved); mySearch.setTotalCount(myCountSaved);
mySearch.setStatus(SearchStatusEnum.FINISHED);
} }
} }
mySearch.setNumFound(myCountSaved); mySearch.setNumFound(myCountSaved);
@ -643,7 +654,16 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
}); });
myInitialCollectionLatch.countDown(); int numSynced;
synchronized (mySyncedPids) {
numSynced = mySyncedPids.size();
}
if (myDaoConfig.getCountSearchResultsUpTo() == null ||
myDaoConfig.getCountSearchResultsUpTo() <= 0 ||
myDaoConfig.getCountSearchResultsUpTo() <= numSynced) {
myInitialCollectionLatch.countDown();
}
} }
} }

View File

@ -20,8 +20,12 @@ package ca.uhn.fhir.jpa.search;
* #L% * #L%
*/ */
import java.util.Date; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@ -34,11 +38,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.annotations.VisibleForTesting; import java.util.Date;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.entity.Search;
/** /**
* Deletes old searches * Deletes old searches
@ -46,35 +46,32 @@ import ca.uhn.fhir.jpa.entity.Search;
public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
public static final long DEFAULT_CUTOFF_SLACK = 10 * DateUtils.MILLIS_PER_SECOND; public static final long DEFAULT_CUTOFF_SLACK = 10 * DateUtils.MILLIS_PER_SECOND;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StaleSearchDeletingSvcImpl.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StaleSearchDeletingSvcImpl.class);
private static Long ourNowForUnitTests;
/* /*
* We give a bit of extra leeway just to avoid race conditions where a query result * We give a bit of extra leeway just to avoid race conditions where a query result
* is being reused (because a new client request came in with the same params) right before * is being reused (because a new client request came in with the same params) right before
* the result is to be deleted * the result is to be deleted
*/ */
private long myCutoffSlack = DEFAULT_CUTOFF_SLACK; private long myCutoffSlack = DEFAULT_CUTOFF_SLACK;
@Autowired @Autowired
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Autowired @Autowired
private ISearchDao mySearchDao; private ISearchDao mySearchDao;
@Autowired @Autowired
private ISearchIncludeDao mySearchIncludeDao; private ISearchIncludeDao mySearchIncludeDao;
@Autowired @Autowired
private ISearchResultDao mySearchResultDao; private ISearchResultDao mySearchResultDao;
@Autowired @Autowired
private PlatformTransactionManager myTransactionManager; private PlatformTransactionManager myTransactionManager;
private void deleteSearch(final Long theSearchPid) { private void deleteSearch(final Long theSearchPid) {
Search searchToDelete = mySearchDao.findOne(theSearchPid); Search searchToDelete = mySearchDao.findOne(theSearchPid);
ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), searchToDelete.getCreated(), searchToDelete.getSearchLastReturned()); if (searchToDelete != null) {
mySearchIncludeDao.deleteForSearch(searchToDelete.getId()); ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), searchToDelete.getCreated(), searchToDelete.getSearchLastReturned());
mySearchResultDao.deleteForSearch(searchToDelete.getId()); mySearchIncludeDao.deleteForSearch(searchToDelete.getId());
mySearchDao.delete(searchToDelete); mySearchResultDao.deleteForSearch(searchToDelete.getId());
mySearchDao.delete(searchToDelete);
}
} }
@Override @Override
@ -85,7 +82,7 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) { if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
cutoffMillis = Math.max(cutoffMillis, myDaoConfig.getReuseCachedSearchResultsForMillis()); cutoffMillis = Math.max(cutoffMillis, myDaoConfig.getReuseCachedSearchResultsForMillis());
} }
final Date cutoff = new Date((System.currentTimeMillis() - cutoffMillis) - myCutoffSlack); final Date cutoff = new Date((now() - cutoffMillis) - myCutoffSlack);
ourLog.debug("Searching for searches which are before {}", cutoff); ourLog.debug("Searching for searches which are before {}", cutoff);
@ -129,4 +126,19 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
myCutoffSlack = theCutoffSlack; myCutoffSlack = theCutoffSlack;
} }
private static long now() {
if (ourNowForUnitTests != null) {
return ourNowForUnitTests;
}
return System.currentTimeMillis();
}
/**
* This is for unit tests only, do not call otherwise
*/
@VisibleForTesting
public static void setNowForUnitTests(Long theNowForUnitTests) {
ourNowForUnitTests = theNowForUnitTests;
}
} }

View File

@ -156,13 +156,11 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
try { try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM); from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
bodyTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_BODY_TEMPLATE);
} catch (FHIRException theE) { } catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
} }
retVal.getEmailDetails().setFrom(from); retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
retVal.getEmailDetails().setBodyTemplate(bodyTemplate);
} }
} catch (FHIRException theE) { } catch (FHIRException theE) {
@ -191,13 +189,11 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
try { try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM); from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
bodyTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_BODY_TEMPLATE);
} catch (FHIRException theE) { } catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
} }
retVal.getEmailDetails().setFrom(from); retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
retVal.getEmailDetails().setBodyTemplate(bodyTemplate);
} }
List<org.hl7.fhir.r4.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); List<org.hl7.fhir.r4.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
@ -459,7 +455,7 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
} }
if (mySubscriptionActivatingSubscriber == null) { if (mySubscriptionActivatingSubscriber == null) {
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this); mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager);
} }
registerSubscriptionCheckingSubscriber(); registerSubscriptionCheckingSubscriber();

View File

@ -131,10 +131,12 @@ public class CanonicalSubscription implements Serializable {
return myHeaders; return myHeaders;
} }
public void setHeaders(String theHeaders) { public void setHeaders(List<? extends IPrimitiveType<String>> theHeader) {
myHeaders = new ArrayList<>(); myHeaders = new ArrayList<>();
if (isNotBlank(theHeaders)) { for (IPrimitiveType<String> next : theHeader) {
myHeaders.add(theHeaders); if (isNotBlank(next.getValueAsString())) {
myHeaders.add(next.getValueAsString());
}
} }
} }
@ -189,12 +191,10 @@ public class CanonicalSubscription implements Serializable {
} }
} }
public void setHeaders(List<? extends IPrimitiveType<String>> theHeader) { public void setHeaders(String theHeaders) {
myHeaders = new ArrayList<>(); myHeaders = new ArrayList<>();
for (IPrimitiveType<String> next : theHeader) { if (isNotBlank(theHeaders)) {
if (isNotBlank(next.getValueAsString())) { myHeaders.add(theHeaders);
myHeaders.add(next.getValueAsString());
}
} }
} }
@ -212,16 +212,6 @@ public class CanonicalSubscription implements Serializable {
private String myFrom; private String myFrom;
@JsonProperty("subjectTemplate") @JsonProperty("subjectTemplate")
private String mySubjectTemplate; private String mySubjectTemplate;
@JsonProperty("bodyTemplate")
private String myBodyTemplate;
public String getBodyTemplate() {
return myBodyTemplate;
}
public void setBodyTemplate(String theBodyTemplate) {
myBodyTemplate = theBodyTemplate;
}
public String getFrom() { public String getFrom() {
return myFrom; return myFrom;

View File

@ -27,20 +27,21 @@ 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.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription;
import org.hl7.fhir.utilities.ucum.Canonical;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException; import org.springframework.messaging.MessagingException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.concurrent.ConcurrentHashMap;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class SubscriptionActivatingSubscriber { public class SubscriptionActivatingSubscriber {
private final IFhirResourceDao mySubscriptionDao; private final IFhirResourceDao mySubscriptionDao;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor; private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
private final PlatformTransactionManager myTransactionManager;
private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class); private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class);
private FhirContext myCtx; private FhirContext myCtx;
private Subscription.SubscriptionChannelType myChannelType; private Subscription.SubscriptionChannelType myChannelType;
@ -48,11 +49,12 @@ public class SubscriptionActivatingSubscriber {
/** /**
* Constructor * Constructor
*/ */
public SubscriptionActivatingSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { public SubscriptionActivatingSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, PlatformTransactionManager theTransactionManager) {
mySubscriptionDao = theSubscriptionDao; mySubscriptionDao = theSubscriptionDao;
mySubscriptionInterceptor = theSubscriptionInterceptor; mySubscriptionInterceptor = theSubscriptionInterceptor;
myChannelType = theChannelType; myChannelType = theChannelType;
myCtx = theSubscriptionDao.getContext(); myCtx = theSubscriptionDao.getContext();
myTransactionManager = theTransactionManager;
} }
public void activateAndRegisterSubscriptionIfRequired(final IBaseResource theSubscription) { public void activateAndRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
@ -67,15 +69,16 @@ public class SubscriptionActivatingSubscriber {
final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode(); final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode();
final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode(); final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode();
if (requestedStatus.equals(statusString)) { if (requestedStatus.equals(statusString)) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { if (TransactionSynchronizationManager.isSynchronizationActive()) {
@Override TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
public void afterCommit() { @Override
status.setValueAsString(activeStatus); public void afterCommit() {
ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), requestedStatus, activeStatus); activateSubscription(status, activeStatus, theSubscription, requestedStatus);
mySubscriptionDao.update(theSubscription); }
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription); });
} } else {
}); activateSubscription(status, activeStatus, theSubscription, requestedStatus);
}
} else if (activeStatus.equals(statusString)) { } else if (activeStatus.equals(statusString)) {
if (!mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) { if (!mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) {
ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
@ -89,8 +92,15 @@ public class SubscriptionActivatingSubscriber {
} }
} }
private void activateSubscription(IPrimitiveType<?> theStatus, String theActiveStatus, IBaseResource theSubscription, String theRequestedStatus) {
theStatus.setValueAsString(theActiveStatus);
ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus);
mySubscriptionDao.update(theSubscription);
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
}
public void handleMessage(RestOperationTypeEnum theOperationType, IIdType theId, IBaseResource theSubscription) throws MessagingException {
public void handleMessage(RestOperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException {
switch (theOperationType) { switch (theOperationType) {
case DELETE: case DELETE:
@ -101,7 +111,15 @@ public class SubscriptionActivatingSubscriber {
if (!theId.getResourceType().equals("Subscription")) { if (!theId.getResourceType().equals("Subscription")) {
return; return;
} }
activateAndRegisterSubscriptionIfRequired(theSubscription); TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
activateAndRegisterSubscriptionIfRequired(theSubscription);
}
});
break;
default:
break; break;
} }

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.subscription.email;
* #L% * #L%
*/ */
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List; import java.util.List;
public class EmailDetails { public class EmailDetails {
@ -27,6 +29,7 @@ public class EmailDetails {
private String myBodyTemplate; private String myBodyTemplate;
private List<String> myTo; private List<String> myTo;
private String myFrom; private String myFrom;
private IIdType mySubscription;
public String getBodyTemplate() { public String getBodyTemplate() {
return myBodyTemplate; return myBodyTemplate;
@ -52,6 +55,14 @@ public class EmailDetails {
mySubjectTemplate = theSubjectTemplate; mySubjectTemplate = theSubjectTemplate;
} }
public IIdType getSubscription() {
return mySubscription;
}
public void setSubscription(IIdType theSubscription) {
mySubscription = theSubscription;
}
public List<String> getTo() { public List<String> getTo() {
return myTo; return myTo;
} }

View File

@ -22,11 +22,11 @@ package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thymeleaf.context.Context; import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.SpringTemplateEngine;
@ -35,6 +35,9 @@ import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver; import org.thymeleaf.templateresolver.StringTemplateResolver;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -42,26 +45,57 @@ import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim; import static org.apache.commons.lang3.StringUtils.trim;
public class EmailSender implements IEmailSender { public class JavaMailEmailSender implements IEmailSender {
private static final Logger ourLog = LoggerFactory.getLogger(EmailSender.class); private static final Logger ourLog = LoggerFactory.getLogger(JavaMailEmailSender.class);
private String mySmtpServerHost; private String mySmtpServerHostname;
private int mySmtpServerPort = 25; private int mySmtpServerPort = 25;
private JavaMailSenderImpl mySender; private JavaMailSenderImpl mySender;
private String mySmtpServerUsername;
private String mySmtpServerPassword;
@PostConstruct public String getSmtpServerHostname() {
public void start() { return mySmtpServerHostname;
Validate.notBlank(mySmtpServerHost, "No SMTP host defined"); }
mySender = new JavaMailSenderImpl(); /**
mySender.setHost(mySmtpServerHost); * Set the SMTP server host to use for outbound mail
mySender.setPort(mySmtpServerPort); */
mySender.setDefaultEncoding(Constants.CHARSET_UTF8.name()); public void setSmtpServerHostname(String theSmtpServerHostname) {
mySmtpServerHostname = theSmtpServerHostname;
}
public String getSmtpServerPassword() {
return mySmtpServerPassword;
}
public void setSmtpServerPassword(String theSmtpServerPassword) {
mySmtpServerPassword = theSmtpServerPassword;
}
public int getSmtpServerPort() {
return mySmtpServerPort;
}
/**
* Set the SMTP server port to use for outbound mail
*/
public void setSmtpServerPort(int theSmtpServerPort) {
mySmtpServerPort = theSmtpServerPort;
}
public String getSmtpServerUsername() {
return mySmtpServerUsername;
}
public void setSmtpServerUsername(String theSmtpServerUsername) {
mySmtpServerUsername = theSmtpServerUsername;
} }
@Override @Override
public void send(EmailDetails theDetails) { public void send(EmailDetails theDetails) {
ourLog.info("Sending email to recipients: {}", theDetails.getTo()); String subscriptionId = theDetails.getSubscription().toUnqualifiedVersionless().getValue();
ourLog.info("Sending email for subscription {} to recipients: {}", subscriptionId, theDetails.getTo());
StopWatch sw = new StopWatch(); StopWatch sw = new StopWatch();
StringTemplateResolver templateResolver = new StringTemplateResolver(); StringTemplateResolver templateResolver = new StringTemplateResolver();
@ -80,39 +114,44 @@ public class EmailSender implements IEmailSender {
String body = engine.process(theDetails.getBodyTemplate(), context); String body = engine.process(theDetails.getBodyTemplate(), context);
String subject = engine.process(theDetails.getSubjectTemplate(), context); String subject = engine.process(theDetails.getSubjectTemplate(), context);
SimpleMailMessage email = new SimpleMailMessage(); MimeMessage email = mySender.createMimeMessage();
email.setFrom(trim(theDetails.getFrom()));
email.setTo(toTrimmedStringArray(theDetails.getTo())); try {
email.setSubject(subject); email.setFrom(trim(theDetails.getFrom()));
email.setText(body); email.setRecipients(Message.RecipientType.TO, toTrimmedCommaSeparatedString(theDetails.getTo()));
email.setSentDate(new Date()); email.setSubject(subject);
email.setText(body);
email.setSentDate(new Date());
email.addHeader("X-FHIR-Subscription", subscriptionId);
} catch (MessagingException e) {
throw new InternalErrorException("Failed to create email messaage", e);
}
mySender.send(email); mySender.send(email);
ourLog.info("Done sending email (took {}ms)", sw.getMillis()); ourLog.info("Done sending email (took {}ms)", sw.getMillis());
} }
/** @PostConstruct
* Set the SMTP server host to use for outbound mail public void start() {
*/ Validate.notBlank(mySmtpServerHostname, "No SMTP host defined");
public void setSmtpServerHost(String theSmtpServerHost) {
mySmtpServerHost = theSmtpServerHost; mySender = new JavaMailSenderImpl();
mySender.setHost(getSmtpServerHostname());
mySender.setPort(getSmtpServerPort());
mySender.setUsername(getSmtpServerUsername());
mySender.setPassword(getSmtpServerPassword());
mySender.setDefaultEncoding(Constants.CHARSET_UTF8.name());
} }
/** private static String toTrimmedCommaSeparatedString(List<String> theTo) {
* Set the SMTP server port to use for outbound mail
*/
public void setSmtpServerPort(int theSmtpServerPort) {
mySmtpServerPort = theSmtpServerPort;
}
private static String[] toTrimmedStringArray(List<String> theTo) {
List<String> to = new ArrayList<>(); List<String> to = new ArrayList<>();
for (String next : theTo) { for (String next : theTo) {
if (isNotBlank(next)) { if (isNotBlank(next)) {
to.add(next); to.add(next);
} }
} }
return to.toArray(new String[to.size()]);
return StringUtils.join(to, ",");
} }
} }

View File

@ -54,32 +54,29 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv
List<String> destinationAddresses = new ArrayList<>(); List<String> destinationAddresses = new ArrayList<>();
String[] destinationAddressStrings = StringUtils.split(endpointUrl, ","); String[] destinationAddressStrings = StringUtils.split(endpointUrl, ",");
for (String next : destinationAddressStrings) { for (String next : destinationAddressStrings) {
next = trim(defaultString(next));
if (next.startsWith("mailto:")) {
next = next.substring("mailto:".length());
}
if (isNotBlank(next)) { if (isNotBlank(next)) {
destinationAddresses.add(trim(next)); destinationAddresses.add(next);
} }
} }
String from = defaultString(subscription.getEmailDetails().getFrom(), provideDefaultFrom()); String from = defaultString(subscription.getEmailDetails().getFrom(), mySubscriptionEmailInterceptor.getDefaultFromAddress());
String subjectTemplate = defaultString(subscription.getEmailDetails().getSubjectTemplate(), provideDefaultSubjectTemplate()); String subjectTemplate = defaultString(subscription.getEmailDetails().getSubjectTemplate(), provideDefaultSubjectTemplate());
String bodyTemplate = defaultString(subscription.getEmailDetails().getBodyTemplate(), provideDefaultBodyTemplate());
EmailDetails details = new EmailDetails(); EmailDetails details = new EmailDetails();
details.setTo(destinationAddresses); details.setTo(destinationAddresses);
details.setFrom(from); details.setFrom(from);
details.setBodyTemplate(bodyTemplate); details.setBodyTemplate(subscription.getPayloadString());
details.setSubjectTemplate(subjectTemplate); details.setSubjectTemplate(subjectTemplate);
details.setSubscription(subscription.getIdElement(getContext()));
IEmailSender emailSender = mySubscriptionEmailInterceptor.getEmailSender(); IEmailSender emailSender = mySubscriptionEmailInterceptor.getEmailSender();
emailSender.send(details); emailSender.send(details);
} }
private String provideDefaultBodyTemplate() {
return "A subscription update has been received";
}
private String provideDefaultFrom() {
return "unknown@sender.com";
}
private String provideDefaultSubjectTemplate() { private String provideDefaultSubjectTemplate() {
return "HAPI FHIR Subscriptions"; return "HAPI FHIR Subscriptions";

View File

@ -20,28 +20,51 @@ package ca.uhn.fhir.jpa.subscription.email;
* #L% * #L%
*/ */
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Required; import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.List;
public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor { public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
private SubscriptionDeliveringEmailSubscriber mySubscriptionDeliverySubscriber; private SubscriptionDeliveringEmailSubscriber mySubscriptionDeliverySubscriber;
/**
* This is set to autowired=false just so that implementors can supply this
* with a mechanism other than autowiring if they want
*/
@Autowired(required = false)
private IEmailSender myEmailSender; private IEmailSender myEmailSender;
private String myDefaultFromAddress = "noreply@unknown.com";
@Override @Override
public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() { public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() {
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.EMAIL; return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.EMAIL;
} }
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public String getDefaultFromAddress() {
return myDefaultFromAddress;
}
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public void setDefaultFromAddress(String theDefaultFromAddress) {
Validate.notBlank(theDefaultFromAddress, "theDefaultFromAddress must not be null or blank");
myDefaultFromAddress = theDefaultFromAddress;
}
public IEmailSender getEmailSender() { public IEmailSender getEmailSender() {
return myEmailSender; return myEmailSender;
} }
@Required /**
* Set the email sender (this method does not need to be explicitly called if you
* are using autowiring to supply the sender)
*/
public void setEmailSender(IEmailSender theEmailSender) { public void setEmailSender(IEmailSender theEmailSender) {
myEmailSender = theEmailSender; myEmailSender = theEmailSender;
} }
@ -54,12 +77,12 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber); getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
} }
@PostConstruct // @PostConstruct
public void start() { // public void start() {
Validate.notNull(myEmailSender, "emailSender has not been configured"); // Validate.notNull(myEmailSender, "emailSender has not been configured");
//
super.start(); // super.start();
} // }
@Override @Override
protected void unregisterDeliverySubscriber() { protected void unregisterDeliverySubscriber() {

View File

@ -24,8 +24,20 @@ public class JpaConstants {
public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique"; public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from"; public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template"; public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
public static final String EXT_SUBSCRIPTION_BODY_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-body-template";
} }

View File

@ -42,7 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SubscriptionsRequireManualActivationInterceptorDstu2 extends ServerOperationInterceptorAdapter { public class SubscriptionsRequireManualActivationInterceptorDstu2 extends ServerOperationInterceptorAdapter {
@Autowired @Autowired
@Qualifier("mySubscriptionDaoR4") @Qualifier("mySubscriptionDaoDstu2")
private IFhirResourceDao<Subscription> myDao; private IFhirResourceDao<Subscription> myDao;
@Override @Override

View File

@ -130,4 +130,19 @@ public class TestUtil {
} }
} }
public static void sleepAtLeast(int theMillis) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() <= start + theMillis) {
try {
long timeSinceStarted = System.currentTimeMillis() - start;
long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
ourLog.info("Sleeping for {}ms", timeToSleep);
Thread.sleep(timeToSleep);
} catch (InterruptedException theE) {
theE.printStackTrace();
}
}
}
} }

View File

@ -97,7 +97,7 @@ public class ConnectionWrapper implements Connection {
@Override @Override
public String getClientInfo(String theName) throws SQLException { public String getClientInfo(String theName) throws SQLException {
return getClientInfo(theName); return myWrap.getClientInfo(theName);
} }
@Override @Override

View File

@ -10,19 +10,30 @@ import javax.sql.DataSource;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
import ca.uhn.fhir.jpa.dao.DaoConfig; import javax.persistence.EntityManagerFactory;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import javax.sql.DataSource;
import ca.uhn.fhir.validation.ResultSeverityEnum; import java.sql.Connection;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import java.sql.SQLException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.fail; import static org.junit.Assert.*;
@Configuration @Configuration
@EnableTransactionManagement() @EnableTransactionManagement()
@ -31,11 +42,6 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestDstu3Config.class); static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestDstu3Config.class);
private Exception myLastStackTrace; private Exception myLastStackTrace;
@Bean()
public DaoConfig daoConfig() {
return new DaoConfig();
}
@Bean() @Bean()
public BasicDataSource basicDataSource() { public BasicDataSource basicDataSource() {
BasicDataSource retVal = new BasicDataSource() { BasicDataSource retVal = new BasicDataSource() {
@ -50,7 +56,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
ourLog.error("Exceeded maximum wait for connection", e); ourLog.error("Exceeded maximum wait for connection", e);
logGetConnectionStackTrace(); logGetConnectionStackTrace();
// if ("true".equals(System.getProperty("ci"))) { // if ("true".equals(System.getProperty("ci"))) {
fail("Exceeded maximum wait for connection: "+ e.toString()); fail("Exceeded maximum wait for connection: " + e.toString());
// } // }
// System.exit(1); // System.exit(1);
retVal = null; retVal = null;
@ -100,20 +106,33 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
return retVal; return retVal;
} }
@Bean()
public DaoConfig daoConfig() {
return new DaoConfig();
}
@Bean() @Bean()
@Primary() @Primary()
public DataSource dataSource() { public DataSource dataSource() {
DataSource dataSource = ProxyDataSourceBuilder DataSource dataSource = ProxyDataSourceBuilder
.create(basicDataSource()) .create(basicDataSource())
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") // .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS) .logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS)
.countQuery() .countQuery()
.build(); .build();
return dataSource; return dataSource;
} }
@Bean
public IEmailSender emailSender() {
JavaMailEmailSender retVal = new JavaMailEmailSender();
retVal.setSmtpServerHostname("localhost");
retVal.setSmtpServerPort(3025);
return retVal;
}
@Bean() @Bean()
public LocalContainerEntityManagerFactoryBean entityManagerFactory() { public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean();

View File

@ -118,8 +118,13 @@ public abstract class BaseJpaTest {
protected List<String> toUnqualifiedVersionlessIdValues(IBundleProvider theFound) { protected List<String> toUnqualifiedVersionlessIdValues(IBundleProvider theFound) {
List<String> retVal = new ArrayList<String>(); List<String> retVal = new ArrayList<String>();
int size = theFound.size(); Integer size = theFound.size();
ourLog.info("Found {} results", size); ourLog.info("Found {} results", size);
if (size == null) {
size = 99999;
}
List<IBaseResource> resources = theFound.getResources(0, size); List<IBaseResource> resources = theFound.getResources(0, size);
for (IBaseResource next : resources) { for (IBaseResource next : resources) {
retVal.add(next.getIdElement().toUnqualifiedVersionless().getValue()); retVal.add(next.getIdElement().toUnqualifiedVersionless().getValue());
@ -298,7 +303,7 @@ public abstract class BaseJpaTest {
public static void waitForSize(int theTarget, List<?> theList) { public static void waitForSize(int theTarget, List<?> theList) {
StopWatch sw = new StopWatch(); StopWatch sw = new StopWatch();
while (theList.size() != theTarget && sw.getMillis() < 10000) { while (theList.size() != theTarget && sw.getMillis() <= 15000) {
try { try {
Thread.sleep(50); Thread.sleep(50);
} catch (InterruptedException theE) { } catch (InterruptedException theE) {

View File

@ -120,7 +120,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
protected Object myResourceProviders; protected Object myResourceProviders;
@Autowired @Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc; protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired @Autowired(required = false)
protected IFulltextSearchSvc mySearchDao; protected IFulltextSearchSvc mySearchDao;
@Autowired @Autowired
protected ISearchParamPresenceSvc mySearchParamPresenceSvc; protected ISearchParamPresenceSvc mySearchParamPresenceSvc;

View File

@ -56,6 +56,30 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myDaoConfig.setTreatReferencesAsLogical(new DaoConfig().getTreatReferencesAsLogical()); myDaoConfig.setTreatReferencesAsLogical(new DaoConfig().getTreatReferencesAsLogical());
} }
/**
* See #773
*/
@Test
public void testDeleteResourceWithOutboundDeletedResources() {
myDaoConfig.setEnforceReferentialIntegrityOnDelete(false);
Organization org = new Organization();
org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org);
Patient pat = new Patient();
pat.setId("PAT");
pat.setActive(true);
pat.setManagingOrganization(new ResourceReferenceDt("Organization/ORG"));
myPatientDao.update(pat);
myOrganizationDao.delete(new IdDt("Organization/ORG"));
myPatientDao.delete(new IdDt("Patient/PAT"));
}
private void assertGone(IIdType theId) { private void assertGone(IIdType theId) {
try { try {
assertNotGone(theId); assertNotGone(theId);

View File

@ -102,6 +102,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
case XML: case XML:
encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); encoded = myFhirCtx.newXmlParser().encodeResourceToString(input);
try { try {
@ -110,6 +111,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
} }
throw new IllegalStateException(); // shouldn't get here throw new IllegalStateException(); // shouldn't get here

View File

@ -172,7 +172,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
protected IResourceTagDao myResourceTagDao; protected IResourceTagDao myResourceTagDao;
@Autowired @Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc; protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired @Autowired(required = false)
protected IFulltextSearchSvc mySearchDao; protected IFulltextSearchSvc mySearchDao;
@Autowired @Autowired
protected ISearchDao mySearchEntityDao; protected ISearchDao mySearchEntityDao;

View File

@ -1,12 +1,16 @@
package ca.uhn.fhir.jpa.dao.dstu3; package ca.uhn.fhir.jpa.dao.dstu3;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import ca.uhn.fhir.jpa.util.StopWatch;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*; import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.util.AopTestUtils; import org.springframework.test.util.AopTestUtils;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionCallbackWithoutResult;
@ -18,15 +22,13 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.StringParam;
public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
@Before private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoDstu3SearchPageExpiryTest.class);
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@After() @After()
public void after() { public void after() {
StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc); StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc);
staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK); staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK);
StaleSearchDeletingSvcImpl.setNowForUnitTests(null);
} }
@Before @Before
@ -35,48 +37,9 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
staleSearchDeletingSvc.setCutoffSlackForUnitTest(0); staleSearchDeletingSvc.setCutoffSlackForUnitTest(0);
} }
@Test @Before
public void testExpirePagesAfterSingleUse() throws Exception { public void beforeDisableResultReuse() {
IIdType pid1; myDaoConfig.setReuseCachedSearchResultsForMillis(null);
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
final IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
myDaoConfig.setExpireSearchResultsAfterMillis(500);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
Thread.sleep(750);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
} }
@Test @Test
@ -98,6 +61,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
myDaoConfig.setExpireSearchResultsAfterMillis(1000L); myDaoConfig.setExpireSearchResultsAfterMillis(1000L);
myDaoConfig.setReuseCachedSearchResultsForMillis(500L); myDaoConfig.setReuseCachedSearchResultsForMillis(500L);
long start = System.currentTimeMillis();
final String searchUuid1; final String searchUuid1;
{ {
@ -109,7 +73,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
Validate.notBlank(searchUuid1); Validate.notBlank(searchUuid1);
} }
Thread.sleep(250); sleepAtLeast(250);
String searchUuid2; String searchUuid2;
{ {
@ -122,7 +86,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
} }
assertEquals(searchUuid1, searchUuid2); assertEquals(searchUuid1, searchUuid2);
Thread.sleep(500); sleepAtLeast(500);
// We're now past 500ms so we shouldn't reuse the search // We're now past 500ms so we shouldn't reuse the search
@ -139,18 +103,31 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
// Search just got used so it shouldn't be deleted // Search just got used so it shouldn't be deleted
Thread.sleep(750); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull(mySearchEntityDao.findByUuid(searchUuid3));
}
});
StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 1400);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() { newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) { protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(searchUuid1));
assertNotNull(mySearchEntityDao.findByUuid(searchUuid3)); assertNotNull(mySearchEntityDao.findByUuid(searchUuid3));
} }
}); });
newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(searchUuid1));
}
});
Thread.sleep(300); StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 2200);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() { newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@ -162,4 +139,63 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test {
}); });
} }
@Test
public void testExpirePagesAfterSingleUse() throws Exception {
IIdType pid1;
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
final StopWatch sw = new StopWatch();
long start = System.currentTimeMillis();
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
final IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
myDaoConfig.setExpireSearchResultsAfterMillis(500);
StaleSearchDeletingSvcImpl.setNowForUnitTests(start);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull("Failed after " + sw.toString(), mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 499);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 600);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
}
} }

View File

@ -133,6 +133,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
case XML: case XML:
encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); encoded = myFhirCtx.newXmlParser().encodeResourceToString(input);
try { try {
@ -141,6 +142,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
} }
throw new IllegalStateException(); // shouldn't get here throw new IllegalStateException(); // shouldn't get here

View File

@ -1879,6 +1879,91 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertNull(nextEntry.getResource()); assertNull(nextEntry.getResource());
} }
@Test
public void testTransactionWithIfMatch() {
Patient p = new Patient();
p.setId("P1");
p.setActive(true);
myPatientDao.update(p);
p.setActive(false);
Bundle b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient/P1")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/P1")
.setIfMatch("2");
try {
mySystemDao.transaction(mySrd, b);
} catch (ResourceVersionConflictException e) {
assertEquals("Trying to update Patient/P1/_history/2 but this is not the current version", e.getMessage());
}
b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient/P1")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/P1")
.setIfMatch("1");
Bundle resp = mySystemDao.transaction(mySrd, b);
assertEquals("Patient/P1/_history/2", new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue());
}
@Test
public void testTransactionWithIfNoneExist() {
Patient p = new Patient();
p.setId("P1");
p.setActive(true);
myPatientDao.update(p);
p = new Patient();
p.setActive(true);
p.addName().setFamily("AAA");
Bundle b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient/P1")
.setIfNoneExist("Patient?active=true");
Bundle resp = mySystemDao.transaction(mySrd, b);
assertEquals("Patient/P1/_history/1", new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue());
p = new Patient();
p.setActive(true);
p.addName().setFamily("AAA");
b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient/P1")
.setIfNoneExist("Patient?active=false");
resp = mySystemDao.transaction(mySrd, b);
assertThat( new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue(), matchesPattern("Patient/[0-9]+/_history/1"));
}
@Test @Test
public void testTransactionSearchWithCount() { public void testTransactionSearchWithCount() {
String methodName = "testTransactionSearchWithCount"; String methodName = "testTransactionSearchWithCount";

View File

@ -181,7 +181,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
protected IResourceTagDao myResourceTagDao; protected IResourceTagDao myResourceTagDao;
@Autowired @Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc; protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired @Autowired(required = false)
protected IFulltextSearchSvc mySearchDao; protected IFulltextSearchSvc mySearchDao;
@Autowired @Autowired
protected ISearchDao mySearchEntityDao; protected ISearchDao mySearchEntityDao;

View File

@ -1,33 +1,35 @@
package ca.uhn.fhir.jpa.dao.r4; package ca.uhn.fhir.jpa.dao.r4;
import static org.hamcrest.Matchers.containsInAnyOrder; import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import static org.junit.Assert.*; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*; import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.util.AopTestUtils; import org.springframework.test.util.AopTestUtils;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.dao.SearchParameterMap; import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import static org.hamcrest.Matchers.containsInAnyOrder;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import static org.junit.Assert.*;
import ca.uhn.fhir.rest.param.StringParam;
public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
@Before private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4SearchPageExpiryTest.class);
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@After() @After()
public void after() { public void after() {
StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc); StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc);
staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK); staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK);
StaleSearchDeletingSvcImpl.setNowForUnitTests(null);
} }
@Before @Before
@ -36,50 +38,9 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
staleSearchDeletingSvc.setCutoffSlackForUnitTest(0); staleSearchDeletingSvc.setCutoffSlackForUnitTest(0);
} }
@Test @Before
public void testExpirePagesAfterSingleUse() throws Exception { public void beforeDisableResultReuse() {
IIdType pid1; myDaoConfig.setReuseCachedSearchResultsForMillis(null);
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
final StopWatch sw = new StopWatch();
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
final IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
myDaoConfig.setExpireSearchResultsAfterMillis(500);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull("Failed after " + sw.toString(), mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
Thread.sleep(750);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
} }
@Test @Test
@ -101,6 +62,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
myDaoConfig.setExpireSearchResultsAfterMillis(1000L); myDaoConfig.setExpireSearchResultsAfterMillis(1000L);
myDaoConfig.setReuseCachedSearchResultsForMillis(500L); myDaoConfig.setReuseCachedSearchResultsForMillis(500L);
long start = System.currentTimeMillis();
final String searchUuid1; final String searchUuid1;
{ {
@ -112,7 +74,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
Validate.notBlank(searchUuid1); Validate.notBlank(searchUuid1);
} }
Thread.sleep(250); sleepAtLeast(250);
String searchUuid2; String searchUuid2;
{ {
@ -125,7 +87,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
} }
assertEquals(searchUuid1, searchUuid2); assertEquals(searchUuid1, searchUuid2);
Thread.sleep(500); sleepAtLeast(500);
// We're now past 500ms so we shouldn't reuse the search // We're now past 500ms so we shouldn't reuse the search
@ -150,7 +112,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
} }
}); });
Thread.sleep(750); StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 1400);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() { newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@ -166,7 +128,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
} }
}); });
Thread.sleep(300); StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 2200);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() { newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@ -178,4 +140,63 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
}); });
} }
@Test
public void testExpirePagesAfterSingleUse() throws Exception {
IIdType pid1;
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().setFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
final StopWatch sw = new StopWatch();
long start = System.currentTimeMillis();
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
final IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
myDaoConfig.setExpireSearchResultsAfterMillis(500);
StaleSearchDeletingSvcImpl.setNowForUnitTests(start);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull("Failed after " + sw.toString(), mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 499);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNotNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 600);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
assertNull(mySearchEntityDao.findByUuid(bundleProvider.getUuid()));
}
});
}
} }

View File

@ -55,6 +55,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
public final void after() { public final void after() {
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
myDaoConfig.setTreatReferencesAsLogical(new DaoConfig().getTreatReferencesAsLogical()); myDaoConfig.setTreatReferencesAsLogical(new DaoConfig().getTreatReferencesAsLogical());
myDaoConfig.setEnforceReferentialIntegrityOnDelete(new DaoConfig().isEnforceReferentialIntegrityOnDelete());
} }
private void assertGone(IIdType theId) { private void assertGone(IIdType theId) {
@ -98,6 +99,29 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
} }
/**
* See #773
*/
@Test
public void testDeleteResourceWithOutboundDeletedResources() {
myDaoConfig.setEnforceReferentialIntegrityOnDelete(false);
Organization org = new Organization();
org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org);
Patient pat = new Patient();
pat.setId("PAT");
pat.setActive(true);
pat.setManagingOrganization(new Reference("Organization/ORG"));
myPatientDao.update(pat);
myOrganizationDao.delete(new IdType("Organization/ORG"));
myPatientDao.delete(new IdType("Patient/PAT"));
}
@Before @Before
public void beforeDisableResultReuse() { public void beforeDisableResultReuse() {

View File

@ -133,6 +133,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
case XML: case XML:
encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); encoded = myFhirCtx.newXmlParser().encodeResourceToString(input);
try { try {
@ -141,6 +142,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
} catch (PreconditionFailedException e) { } catch (PreconditionFailedException e) {
return (OperationOutcome) e.getOperationOutcome(); return (OperationOutcome) e.getOperationOutcome();
} }
break;
} }
throw new IllegalStateException(); // shouldn't get here throw new IllegalStateException(); // shouldn't get here

View File

@ -587,6 +587,90 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
} }
@Test
public void testTransactionWithIfMatch() {
Patient p = new Patient();
p.setId("P1");
p.setActive(true);
myPatientDao.update(p);
p.setActive(false);
Bundle b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient/P1")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/P1")
.setIfMatch("2");
try {
mySystemDao.transaction(mySrd, b);
} catch (ResourceVersionConflictException e) {
assertEquals("Trying to update Patient/P1/_history/2 but this is not the current version", e.getMessage());
}
b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient/P1")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/P1")
.setIfMatch("1");
Bundle resp = mySystemDao.transaction(mySrd, b);
assertEquals("Patient/P1/_history/2", new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue());
}
@Test
public void testTransactionWithIfNoneExist() {
Patient p = new Patient();
p.setId("P1");
p.setActive(true);
myPatientDao.update(p);
p = new Patient();
p.setActive(true);
p.addName().setFamily("AAA");
Bundle b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient/P1")
.setIfNoneExist("Patient?active=true");
Bundle resp = mySystemDao.transaction(mySrd, b);
assertEquals("Patient/P1/_history/1", new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue());
p = new Patient();
p.setActive(true);
p.addName().setFamily("AAA");
b = new Bundle();
b.setType(BundleType.TRANSACTION);
b.addEntry()
.setFullUrl("Patient")
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient/P1")
.setIfNoneExist("Patient?active=false");
resp = mySystemDao.transaction(mySrd, b);
assertThat( new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualified().getValue(), matchesPattern("Patient/[0-9]+/_history/1"));
}
@Test @Test
public void testTransaction1() throws IOException { public void testTransaction1() throws IOException {
String inputBundleString = loadClasspath("/david-bundle-error.json"); String inputBundleString = loadClasspath("/david-bundle-error.json");

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -58,6 +59,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
protected static SearchParamRegistryDstu3 ourSearchParamRegistry; protected static SearchParamRegistryDstu3 ourSearchParamRegistry;
protected static DatabaseBackedPagingProvider ourPagingProvider; protected static DatabaseBackedPagingProvider ourPagingProvider;
protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor; protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor;
protected static SubscriptionEmailInterceptor ourEmailSubscriptionInterceptor;
protected static ISearchDao mySearchEntityDao; protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc; protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
@ -111,13 +113,9 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
ourWebApplicationContext = new GenericWebApplicationContext(); ourWebApplicationContext = new GenericWebApplicationContext();
ourWebApplicationContext.setParent(myAppCtx); ourWebApplicationContext.setParent(myAppCtx);
ourWebApplicationContext.refresh(); ourWebApplicationContext.refresh();
// ContextLoaderListener loaderListener = new ContextLoaderListener(webApplicationContext);
// loaderListener.initWebApplicationContext(mock(ServletContext.class));
//
proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext); proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext);
DispatcherServlet dispatcherServlet = new DispatcherServlet(); DispatcherServlet dispatcherServlet = new DispatcherServlet();
// dispatcherServlet.setApplicationContext(webApplicationContext);
dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class); dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class);
ServletHolder subsServletHolder = new ServletHolder(); ServletHolder subsServletHolder = new ServletHolder();
subsServletHolder.setServlet(dispatcherServlet); subsServletHolder.setServlet(dispatcherServlet);
@ -150,6 +148,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class);
mySearchEntityDao = wac.getBean(ISearchDao.class); mySearchEntityDao = wac.getBean(ISearchDao.class);
ourRestHookSubscriptionInterceptor = wac.getBean(SubscriptionRestHookInterceptor.class); ourRestHookSubscriptionInterceptor = wac.getBean(SubscriptionRestHookInterceptor.class);
ourEmailSubscriptionInterceptor = wac.getBean(SubscriptionEmailInterceptor.class);
ourSearchParamRegistry = wac.getBean(SearchParamRegistryDstu3.class); ourSearchParamRegistry = wac.getBean(SearchParamRegistryDstu3.class);
myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000); myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000);

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.jpa.provider.dstu3; package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.provider.r4.ResourceProviderInterceptorR4Test; import ca.uhn.fhir.jpa.provider.r4.ResourceProviderInterceptorR4Test;
import ca.uhn.fhir.model.dstu2.resource.Conformance;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
@ -61,8 +60,8 @@ public class ResourceProviderInterceptorDstu3Test extends BaseResourceProviderDs
public void before() throws Exception { public void before() throws Exception {
super.before(); super.before();
myServerInterceptor = mock(IServerInterceptor.class); myServerInterceptor = mock(IServerInterceptor.class, withSettings().verboseLogging());
myDaoInterceptor = mock(IServerInterceptor.class); myDaoInterceptor = mock(IServerInterceptor.class, withSettings().verboseLogging());
resetServerInterceptor(); resetServerInterceptor();
@ -125,6 +124,8 @@ public class ResourceProviderInterceptorDstu3Test extends BaseResourceProviderDs
public void testCreateResourceInTransaction() throws IOException, ServletException { public void testCreateResourceInTransaction() throws IOException, ServletException {
String methodName = "testCreateResourceInTransaction"; String methodName = "testCreateResourceInTransaction";
ourLog.info("** Starting {}", methodName);
Patient pt = new Patient(); Patient pt = new Patient();
pt.addName().setFamily(methodName); pt.addName().setFamily(methodName);
@ -140,6 +141,11 @@ public class ResourceProviderInterceptorDstu3Test extends BaseResourceProviderDs
resetServerInterceptor(); resetServerInterceptor();
ArgumentCaptor<ActionRequestDetails> ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<RestOperationTypeEnum> opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myDaoInterceptor, times(0)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
verify(myServerInterceptor, times(0)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
HttpPost post = new HttpPost(ourServerBase + "/"); HttpPost post = new HttpPost(ourServerBase + "/");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post); CloseableHttpResponse response = ourHttpClient.execute(post);
@ -153,8 +159,8 @@ public class ResourceProviderInterceptorDstu3Test extends BaseResourceProviderDs
* Server Interceptor * Server Interceptor
*/ */
ArgumentCaptor<ActionRequestDetails> ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class); ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<RestOperationTypeEnum> opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class); opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myServerInterceptor, times(2)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture()); verify(myServerInterceptor, times(2)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0)); assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0));
assertEquals(null, ardCaptor.getAllValues().get(0).getResourceType()); assertEquals(null, ardCaptor.getAllValues().get(0).getResourceType());

View File

@ -9,6 +9,8 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.apache.http.client.methods.*; import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
@ -69,19 +71,68 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType id = ourClient.create().resource(patient).execute().getId(); IIdType id = myClient.create().resource(patient).execute().getId();
try { try {
ourClient.delete().resourceById(id.toUnqualifiedVersionless()).execute(); myClient.delete().resourceById(id.toUnqualifiedVersionless()).execute();
fail(); fail();
} catch (ForbiddenOperationException e) { } catch (ForbiddenOperationException e) {
// good // good
} }
patient = ourClient.read().resource(Patient.class).withId(id.toUnqualifiedVersionless()).execute(); patient = myClient.read().resource(Patient.class).withId(id.toUnqualifiedVersionless()).execute();
assertEquals(id.getValue(), patient.getId()); assertEquals(id.getValue(), patient.getId());
} }
/**
* See #751
*/
@Test
public void testDeleteInCompartmentIsBlocked() {
Patient patient = new Patient();
patient.setId("Patient/A");
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType id = myClient.update().resource(patient).execute().getId();
Observation obs = new Observation();
obs.setId("Observation/B");
obs.getSubject().setReference("Patient/A");
myClient.update().resource(obs).execute();
obs = new Observation();
obs.setId("Observation/C");
obs.setStatus(ObservationStatus.FINAL);
myClient.update().resource(obs).execute();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().delete().allResources().inCompartment("Patient", new IdType("Patient/A")).andThen()
.allow().read().allResources().withAnyId().andThen()
.denyAll()
.build();
}
});
myClient.delete().resourceById(new IdType("Observation/B")).execute();
try {
myClient.read().resource(Observation.class).withId("Observation/B").execute();
fail();
} catch (ResourceGoneException e) {
// good
}
try {
myClient.delete().resourceById(new IdType("Observation/C")).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
/** /**
* See #503 * See #503
@ -92,16 +143,16 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
final IIdType id = ourClient.create().resource(patient).execute().getId(); final IIdType id = myClient.create().resource(patient).execute().getId();
Observation obsInCompartment = new Observation(); Observation obsInCompartment = new Observation();
obsInCompartment.setStatus(ObservationStatus.FINAL); obsInCompartment.setStatus(ObservationStatus.FINAL);
obsInCompartment.getSubject().setReferenceElement(id.toUnqualifiedVersionless()); obsInCompartment.getSubject().setReferenceElement(id.toUnqualifiedVersionless());
IIdType obsInCompartmentId = ourClient.create().resource(obsInCompartment).execute().getId().toUnqualifiedVersionless(); IIdType obsInCompartmentId = myClient.create().resource(obsInCompartment).execute().getId().toUnqualifiedVersionless();
Observation obsNotInCompartment = new Observation(); Observation obsNotInCompartment = new Observation();
obsNotInCompartment.setStatus(ObservationStatus.FINAL); obsNotInCompartment.setStatus(ObservationStatus.FINAL);
IIdType obsNotInCompartmentId = ourClient.create().resource(obsNotInCompartment).execute().getId().toUnqualifiedVersionless(); IIdType obsNotInCompartmentId = myClient.create().resource(obsNotInCompartment).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override @Override
@ -114,10 +165,10 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
} }
}); });
ourClient.delete().resourceById(obsInCompartmentId.toUnqualifiedVersionless()).execute(); myClient.delete().resourceById(obsInCompartmentId.toUnqualifiedVersionless()).execute();
try { try {
ourClient.delete().resourceById(obsNotInCompartmentId.toUnqualifiedVersionless()).execute(); myClient.delete().resourceById(obsNotInCompartmentId.toUnqualifiedVersionless()).execute();
fail(); fail();
} catch (ForbiddenOperationException e) { } catch (ForbiddenOperationException e) {
// good // good
@ -130,7 +181,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
final MethodOutcome output1 = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute(); final MethodOutcome output1 = myClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override @Override
@ -148,7 +199,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
patient.setId(output1.getId().toUnqualifiedVersionless()); patient.setId(output1.getId().toUnqualifiedVersionless());
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
MethodOutcome output2 = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute(); MethodOutcome output2 = myClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute();
assertEquals(output1.getId().getIdPart(), output2.getId().getIdPart()); assertEquals(output1.getId().getIdPart(), output2.getId().getIdPart());
@ -156,7 +207,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
try { try {
ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|101").execute(); myClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|101").execute();
fail(); fail();
} catch (ForbiddenOperationException e) { } catch (ForbiddenOperationException e) {
assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage()); assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage());
@ -167,7 +218,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad"); patient.addName().setFamily("Tester").addGiven("Raghad");
try { try {
ourClient.update().resource(patient).execute(); myClient.update().resource(patient).execute();
fail(); fail();
} catch (ForbiddenOperationException e) { } catch (ForbiddenOperationException e) {
assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage()); assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage());
@ -182,11 +233,11 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() throws IOException { public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() throws IOException {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.setActive(true); pt1.setActive(true);
final IIdType pid1 = ourClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless(); final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.setActive(false); pt2.setActive(false);
final IIdType pid2 = ourClient.create().resource(pt2).execute().getId().toUnqualifiedVersionless(); final IIdType pid2 = myClient.create().resource(pt2).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override @Override
@ -200,7 +251,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
Observation obs = new Observation(); Observation obs = new Observation();
obs.setStatus(ObservationStatus.FINAL); obs.setStatus(ObservationStatus.FINAL);
obs.setSubject(new Reference(pid1)); obs.setSubject(new Reference(pid1));
IIdType oid = ourClient.create().resource(obs).execute().getId().toUnqualified(); IIdType oid = myClient.create().resource(obs).execute().getId().toUnqualified();
unregisterInterceptors(); unregisterInterceptors();
@ -224,7 +275,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
obs.setSubject(new Reference(pid2)); obs.setSubject(new Reference(pid2));
try { try {
ourClient.update().resource(obs).execute(); myClient.update().resource(obs).execute();
fail(); fail();
} catch (ForbiddenOperationException e) { } catch (ForbiddenOperationException e) {
// good // good

View File

@ -47,7 +47,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected static JpaValidationSupportChainR4 myValidationSupport; protected static JpaValidationSupportChainR4 myValidationSupport;
protected static IGenericClient ourClient; protected IGenericClient myClient;
protected static CloseableHttpClient ourHttpClient; protected static CloseableHttpClient ourHttpClient;
protected static int ourPort; protected static int ourPort;
protected static RestfulServer ourRestServer; protected static RestfulServer ourRestServer;
@ -150,10 +150,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourSearchParamRegistry = wac.getBean(SearchParamRegistryR4.class); ourSearchParamRegistry = wac.getBean(SearchParamRegistryR4.class);
myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000); myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000);
ourClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
if (shouldLogClient()) {
ourClient.registerInterceptor(new LoggingInterceptor());
}
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create(); HttpClientBuilder builder = HttpClientBuilder.create();
@ -165,6 +161,11 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
} }
ourRestServer.setPagingProvider(ourPagingProvider); ourRestServer.setPagingProvider(ourPagingProvider);
myClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
if (shouldLogClient()) {
myClient.registerInterceptor(new LoggingInterceptor());
}
} }
/** /**

View File

@ -1,11 +1,11 @@
package ca.uhn.fhir.jpa.provider.r4; package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -31,14 +31,14 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
try { try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
assertEquals(resp, "{\n" + assertEquals(TestUtil.stripReturns(resp), TestUtil.stripReturns("{\n" +
" \"name\":[{\n" + " \"name\":[{\n" +
" \"family\":\"FAM\",\n" + " \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" + " },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" + " }]\n" +
"}"); "}"));
} finally { } finally {
IOUtils.closeQuietly(response); IOUtils.closeQuietly(response);
} }
@ -56,7 +56,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
try { try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
assertEquals(resp, "{\n" + assertEquals(TestUtil.stripReturns(resp), TestUtil.stripReturns("{\n" +
" \"PatientList\":[{\n" + " \"PatientList\":[{\n" +
" \"name\":[{\n" + " \"name\":[{\n" +
" \"family\":\"FAM\",\n" + " \"family\":\"FAM\",\n" +
@ -69,7 +69,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
" \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" + " \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" +
" }]\n" + " }]\n" +
" }]\n" + " }]\n" +
"}"); "}"));
} finally { } finally {
IOUtils.closeQuietly(response); IOUtils.closeQuietly(response);
} }
@ -85,13 +85,13 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
p.addName() p.addName()
.addGiven("GivenOnly1") .addGiven("GivenOnly1")
.addGiven("GivenOnly2"); .addGiven("GivenOnly2");
myPatientId0 = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); myPatientId0 = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
p = new Patient(); p = new Patient();
p.addName() p.addName()
.addGiven("GivenOnlyB1") .addGiven("GivenOnlyB1")
.addGiven("GivenOnlyB2"); .addGiven("GivenOnlyB2");
ourClient.create().resource(p).execute(); myClient.create().resource(p).execute();
} }

View File

@ -57,41 +57,41 @@ public class PatientEverythingR4Test extends BaseResourceProviderR4Test {
Organization org = new Organization(); Organization org = new Organization();
org.setName("an org"); org.setName("an org");
orgId = ourClient.create().resource(org).execute().getId().toUnqualifiedVersionless().getValue(); orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless().getValue();
ourLog.info("OrgId: {}", orgId); ourLog.info("OrgId: {}", orgId);
Patient patient = new Patient(); Patient patient = new Patient();
patient.getManagingOrganization().setReference(orgId); patient.getManagingOrganization().setReference(orgId);
patId = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless().getValue(); patId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless().getValue();
Patient patient2 = new Patient(); Patient patient2 = new Patient();
patient2.getManagingOrganization().setReference(orgId); patient2.getManagingOrganization().setReference(orgId);
myWrongPatId = ourClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue(); myWrongPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue();
Encounter enc1 = new Encounter(); Encounter enc1 = new Encounter();
enc1.setStatus(EncounterStatus.CANCELLED); enc1.setStatus(EncounterStatus.CANCELLED);
enc1.getSubject().setReference(patId); enc1.getSubject().setReference(patId);
enc1.getServiceProvider().setReference(orgId); enc1.getServiceProvider().setReference(orgId);
encId1 = ourClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless().getValue(); encId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless().getValue();
Encounter enc2 = new Encounter(); Encounter enc2 = new Encounter();
enc2.setStatus(EncounterStatus.ARRIVED); enc2.setStatus(EncounterStatus.ARRIVED);
enc2.getSubject().setReference(patId); enc2.getSubject().setReference(patId);
enc2.getServiceProvider().setReference(orgId); enc2.getServiceProvider().setReference(orgId);
encId2 = ourClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless().getValue(); encId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless().getValue();
Encounter wrongEnc1 = new Encounter(); Encounter wrongEnc1 = new Encounter();
wrongEnc1.setStatus(EncounterStatus.ARRIVED); wrongEnc1.setStatus(EncounterStatus.ARRIVED);
wrongEnc1.getSubject().setReference(myWrongPatId); wrongEnc1.getSubject().setReference(myWrongPatId);
wrongEnc1.getServiceProvider().setReference(orgId); wrongEnc1.getServiceProvider().setReference(orgId);
myWrongEnc1 = ourClient.create().resource(wrongEnc1).execute().getId().toUnqualifiedVersionless().getValue(); myWrongEnc1 = myClient.create().resource(wrongEnc1).execute().getId().toUnqualifiedVersionless().getValue();
myObsIds = new ArrayList<String>(); myObsIds = new ArrayList<String>();
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
Observation obs = new Observation(); Observation obs = new Observation();
obs.getSubject().setReference(patId); obs.getSubject().setReference(patId);
obs.setStatus(ObservationStatus.FINAL); obs.setStatus(ObservationStatus.FINAL);
String obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless().getValue(); String obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless().getValue();
myObsIds.add(obsId); myObsIds.add(obsId);
} }

View File

@ -54,7 +54,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
sp.setTitle("Foo Param"); sp.setTitle("Foo Param");
try { try {
ourClient.create().resource(sp).execute(); myClient.create().resource(sp).execute();
fail(); fail();
} catch (UnprocessableEntityException e) { } catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: SearchParameter.status is missing or invalid: null", e.getMessage()); assertEquals("HTTP 422 Unprocessable Entity: SearchParameter.status is missing or invalid: null", e.getMessage());
@ -120,7 +120,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(eyeColourSp)); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(eyeColourSp));
ourClient myClient
.create() .create()
.resource(eyeColourSp) .resource(eyeColourSp)
.execute(); .execute();
@ -139,7 +139,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
p2.addExtension().setUrl("http://acme.org/eyecolour").setValue(new CodeType("green")); p2.addExtension().setUrl("http://acme.org/eyecolour").setValue(new CodeType("green"));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless(); IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
Bundle bundle = ourClient Bundle bundle = myClient
.search() .search()
.forResource(Patient.class) .forResource(Patient.class)
.where(new TokenClientParam("eyecolour").exactly().code("blue")) .where(new TokenClientParam("eyecolour").exactly().code("blue"))
@ -168,7 +168,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
public void testConformanceOverrideAllowed() { public void testConformanceOverrideAllowed() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); myDaoConfig.setDefaultSearchParamsCanBeOverridden(true);
CapabilityStatement conformance = ourClient CapabilityStatement conformance = myClient
.fetchConformance() .fetchConformance()
.ofType(CapabilityStatement.class) .ofType(CapabilityStatement.class)
.execute(); .execute();
@ -220,7 +220,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
} }
}); });
conformance = ourClient conformance = myClient
.fetchConformance() .fetchConformance()
.ofType(CapabilityStatement.class) .ofType(CapabilityStatement.class)
.execute(); .execute();
@ -238,7 +238,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
public void testConformanceOverrideNotAllowed() { public void testConformanceOverrideNotAllowed() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); myDaoConfig.setDefaultSearchParamsCanBeOverridden(false);
CapabilityStatement conformance = ourClient CapabilityStatement conformance = myClient
.fetchConformance() .fetchConformance()
.ofType(CapabilityStatement.class) .ofType(CapabilityStatement.class)
.execute(); .execute();
@ -274,7 +274,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
mySearchParamRegsitry.forceRefresh(); mySearchParamRegsitry.forceRefresh();
conformance = ourClient conformance = myClient
.fetchConformance() .fetchConformance()
.ofType(CapabilityStatement.class) .ofType(CapabilityStatement.class)
.execute(); .execute();
@ -332,7 +332,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
List<String> foundResources; List<String> foundResources;
Bundle result; Bundle result;
result = ourClient result = myClient
.search() .search()
.forResource(Patient.class) .forResource(Patient.class)
.where(new TokenClientParam("foo").exactly().code("male")) .where(new TokenClientParam("foo").exactly().code("male"))
@ -409,7 +409,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
List<String> foundResources; List<String> foundResources;
Bundle result; Bundle result;
result = ourClient result = myClient
.search() .search()
.forResource(Observation.class) .forResource(Observation.class)
.where(new ReferenceClientParam("foo").hasChainedProperty(Patient.GENDER.exactly().code("male"))) .where(new ReferenceClientParam("foo").hasChainedProperty(Patient.GENDER.exactly().code("male")))

View File

@ -198,7 +198,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
Organization org = new Organization(); Organization org = new Organization();
org.setName("orgName"); org.setName("orgName");
IIdType orgId = ourClient.create().resource(org).execute().getId().toUnqualified(); IIdType orgId = myClient.create().resource(org).execute().getId().toUnqualified();
assertNotNull(orgId.getVersionIdPartAsLong()); assertNotNull(orgId.getVersionIdPartAsLong());
resetServerInterceptor(); resetServerInterceptor();

View File

@ -72,7 +72,7 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
qr1.setStatus(QuestionnaireResponseStatus.COMPLETED); qr1.setStatus(QuestionnaireResponseStatus.COMPLETED);
qr1.addItem().setLinkId("link1").addAnswer().setValue(new DecimalType(123)); qr1.addItem().setLinkId("link1").addAnswer().setValue(new DecimalType(123));
try { try {
ourClient.create().resource(qr1).execute(); myClient.create().resource(qr1).execute();
fail(); fail();
} catch (UnprocessableEntityException e) { } catch (UnprocessableEntityException e) {
assertThat(e.toString(), containsString("Answer value must be of type string")); assertThat(e.toString(), containsString("Answer value must be of type string"));
@ -95,7 +95,7 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
qr1.setStatus(QuestionnaireResponseStatus.COMPLETED); qr1.setStatus(QuestionnaireResponseStatus.COMPLETED);
qr1.addItem().setLinkId("link1").addAnswer().setValue(new DecimalType(123)); qr1.addItem().setLinkId("link1").addAnswer().setValue(new DecimalType(123));
try { try {
ourClient.create().resource(qr1).execute(); myClient.create().resource(qr1).execute();
fail(); fail();
} catch (UnprocessableEntityException e) { } catch (UnprocessableEntityException e) {
assertThat(e.toString(), containsString("Answer value must be of type string")); assertThat(e.toString(), containsString("Answer value must be of type string"));

View File

@ -34,9 +34,9 @@ public class ResourceProviderR4BundleTest extends BaseResourceProviderR4Test {
composition.setTitle("Visit Summary"); composition.setTitle("Visit Summary");
bundle.addEntry().setFullUrl("http://foo").setResource(composition); bundle.addEntry().setFullUrl("http://foo").setResource(composition);
IIdType id = ourClient.create().resource(bundle).execute().getId(); IIdType id = myClient.create().resource(bundle).execute().getId();
Bundle retBundle = ourClient.read().resource(Bundle.class).withId(id).execute(); Bundle retBundle = myClient.read().resource(Bundle.class).withId(id).execute();
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle)); ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle));
assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl()); assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl());

View File

@ -4,6 +4,8 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
@ -14,13 +16,17 @@ import org.junit.Test;
import org.springframework.test.util.AopTestUtils; import org.springframework.test.util.AopTestUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test { public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4CacheTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4CacheTest.class);
private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw; private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw;
private CapturingInterceptor myCapturingInterceptor;
@Override @Override
@After @After
@ -28,6 +34,8 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
super.after(); super.after();
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(new DaoConfig().getCacheControlNoStoreMaxResultsUpperLimit()); myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(new DaoConfig().getCacheControlNoStoreMaxResultsUpperLimit());
myClient.unregisterInterceptor(myCapturingInterceptor);
} }
@Override @Override
@ -35,6 +43,9 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
super.before(); super.before();
myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
mySearchCoordinatorSvcRaw = AopTestUtils.getTargetObject(mySearchCoordinatorSvc); mySearchCoordinatorSvcRaw = AopTestUtils.getTargetObject(mySearchCoordinatorSvc);
myCapturingInterceptor = new CapturingInterceptor();
myClient.registerInterceptor(myCapturingInterceptor);
} }
@Test @Test
@ -42,9 +53,9 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.addName().setFamily("FAM"); pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute(); myClient.create().resource(pt1).execute();
Bundle results = ourClient Bundle results = myClient
.search() .search()
.forResource("Patient") .forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM")) .where(Patient.FAMILY.matches().value("FAM"))
@ -53,12 +64,13 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
.execute(); .execute();
assertEquals(1, results.getEntry().size()); assertEquals(1, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count()); assertEquals(0, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.addName().setFamily("FAM"); pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute(); myClient.create().resource(pt2).execute();
results = ourClient results = myClient
.search() .search()
.forResource("Patient") .forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM")) .where(Patient.FAMILY.matches().value("FAM"))
@ -67,6 +79,7 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
.execute(); .execute();
assertEquals(2, results.getEntry().size()); assertEquals(2, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count()); assertEquals(0, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
} }
@ -76,10 +89,10 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.addName().setFamily("FAM" + i); pt1.addName().setFamily("FAM" + i);
ourClient.create().resource(pt1).execute(); myClient.create().resource(pt1).execute();
} }
Bundle results = ourClient Bundle results = myClient
.search() .search()
.forResource("Patient") .forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM")) .where(Patient.FAMILY.matches().value("FAM"))
@ -88,6 +101,7 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
.execute(); .execute();
assertEquals(5, results.getEntry().size()); assertEquals(5, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count()); assertEquals(0, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
} }
@ -95,7 +109,7 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
public void testCacheNoStoreMaxResultsWithIllegalValue() throws IOException { public void testCacheNoStoreMaxResultsWithIllegalValue() throws IOException {
myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(123); myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(123);
try { try {
ourClient myClient
.search() .search()
.forResource("Patient") .forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM")) .where(Patient.FAMILY.matches().value("FAM"))
@ -113,17 +127,18 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.addName().setFamily("FAM"); pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute(); myClient.create().resource(pt1).execute();
Bundle results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute(); Bundle results = myClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results.getEntry().size()); assertEquals(1, results.getEntry().size());
assertEquals(1, mySearchEntityDao.count()); assertEquals(1, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.addName().setFamily("FAM"); pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute(); myClient.create().resource(pt2).execute();
results = ourClient results = myClient
.search() .search()
.forResource("Patient") .forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM")) .where(Patient.FAMILY.matches().value("FAM"))
@ -132,6 +147,7 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
.execute(); .execute();
assertEquals(2, results.getEntry().size()); assertEquals(2, results.getEntry().size());
assertEquals(2, mySearchEntityDao.count()); assertEquals(2, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
} }
@ -140,20 +156,28 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.addName().setFamily("FAM"); pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute(); myClient.create().resource(pt1).execute();
Bundle results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute(); Date beforeFirst = new Date();
assertEquals(1, results.getEntry().size());
Bundle results1 = myClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results1.getEntry().size());
assertEquals(1, mySearchEntityDao.count()); assertEquals(1, mySearchEntityDao.count());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty());
assertThat(results1.getMeta().getLastUpdated(), greaterThan(beforeFirst));
assertThat(results1.getMeta().getLastUpdated(), lessThan(new Date()));
assertThat(results1.getId(), not(blankOrNullString()));
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.addName().setFamily("FAM"); pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute(); myClient.create().resource(pt2).execute();
results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute(); Bundle results2 = myClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results.getEntry().size()); assertEquals(1, results2.getEntry().size());
assertEquals(1, mySearchEntityDao.count()); assertEquals(1, mySearchEntityDao.count());
assertEquals("HIT from " + ourServerBase, myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE).get(0));
assertEquals(results1.getMeta().getLastUpdated(), results2.getMeta().getLastUpdated());
assertEquals(results1.getId(), results2.getId());
} }
@AfterClass @AfterClass

Some files were not shown because too many files have changed in this diff Show More