Add support for Binary X-Security-Context header in server
This commit is contained in:
parent
256d541dda
commit
75bfb6af1b
|
@ -118,6 +118,7 @@
|
||||||
</ignoredDependencies>
|
</ignoredDependencies>
|
||||||
<ignoredResources>
|
<ignoredResources>
|
||||||
<ignoredResource>changelog.txt</ignoredResource>
|
<ignoredResource>changelog.txt</ignoredResource>
|
||||||
|
<ignoredResource>javac.bat</ignoredResource>
|
||||||
</ignoredResources>
|
</ignoredResources>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,10 +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 java.lang.reflect.Method;
|
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
|
||||||
|
@ -140,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);
|
||||||
|
@ -157,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();
|
||||||
|
@ -452,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++;
|
||||||
|
@ -696,34 +700,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theResource instanceof IBaseBinary) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Method getSC = bin.getClass().getMethod("getSecurityContext");
|
|
||||||
Object securityContext = getSC.invoke(bin);
|
|
||||||
if (securityContext != null) {
|
|
||||||
Method getRef = securityContext.getClass().getMethod("getReference");
|
|
||||||
String securityContextRef = (String) getRef.invoke(securityContext);
|
|
||||||
if (securityContextRef != null) {
|
|
||||||
beginObject(theEventWriter, "securityContext");
|
|
||||||
writeOptionalTagWithTextNode(theEventWriter, "reference", securityContextRef);
|
|
||||||
theEventWriter.endObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, theSubResource, new CompositeChildElement(resDef, theSubResource));
|
encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, theSubResource, new CompositeChildElement(resDef, theSubResource));
|
||||||
}
|
|
||||||
|
|
||||||
theEventWriter.endObject();
|
theEventWriter.endObject();
|
||||||
}
|
}
|
||||||
|
@ -1344,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);
|
||||||
|
@ -1445,27 +1444,5 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,7 @@ public class Constants {
|
||||||
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_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);
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
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");
|
||||||
|
|
||||||
|
List<IBase> values = child.getAccessor().getValues(theBinary);
|
||||||
|
IBaseReference retVal = null;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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,26 +46,21 @@ 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;
|
||||||
|
|
||||||
|
@ -87,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);
|
||||||
|
|
||||||
|
@ -131,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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.util.BinaryUtil;
|
||||||
import org.hl7.fhir.instance.model.api.*;
|
import org.hl7.fhir.instance.model.api.*;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
@ -580,10 +581,17 @@ public class RestfulServerUtils {
|
||||||
} else {
|
} else {
|
||||||
contentType = Constants.CT_OCTET_STREAM;
|
contentType = Constants.CT_OCTET_STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force binary resources to download - This is a security measure to prevent
|
// Force binary resources to download - This is a security measure to prevent
|
||||||
// malicious images or HTML blocks being served up as content.
|
// malicious images or HTML blocks being served up as content.
|
||||||
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
|
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
|
||||||
|
|
||||||
|
IBaseReference securityContext = BinaryUtil.getSecurityContext(theServer.getFhirContext(), bin);
|
||||||
|
String securityContextRef = securityContext.getReferenceElement().getValue();
|
||||||
|
if (isNotBlank(securityContextRef)) {
|
||||||
|
response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, securityContextRef);
|
||||||
|
}
|
||||||
|
|
||||||
return response.sendAttachmentResponse(bin, theStausCode, contentType);
|
return response.sendAttachmentResponse(bin, theStausCode, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,24 +149,6 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
||||||
|
|
||||||
return theRequest.getResponse().streamResponseAsResource(response, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), isAddContentLocationHeader());
|
return theRequest.getResponse().streamResponseAsResource(response, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), isAddContentLocationHeader());
|
||||||
|
|
||||||
// DSTU1 Bundle
|
|
||||||
// // Is this request coming from a browser
|
|
||||||
// String uaHeader = theRequest.getHeader("user-agent");
|
|
||||||
// boolean requestIsBrowser = false;
|
|
||||||
// if (uaHeader != null && uaHeader.contains("Mozilla")) {
|
|
||||||
// requestIsBrowser = true;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
|
|
||||||
// IServerInterceptor next = theServer.getInterceptors().get(i);
|
|
||||||
// boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle());
|
|
||||||
// if (!continueProcessing) {
|
|
||||||
// ourLog.debug("Interceptor {} returned false, not continuing processing");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return theRequest.getResponse().streamResponseAsBundle(responseObject.getDstu1Bundle(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
|
public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
|
||||||
|
|
|
@ -19,7 +19,24 @@ package ca.uhn.fhir.rest.server.method;
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.parser.DataFormatException;
|
||||||
|
import ca.uhn.fhir.parser.IParser;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
|
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
|
import ca.uhn.fhir.util.BinaryUtil;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -30,27 +47,8 @@ import java.lang.reflect.Modifier;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import org.apache.commons.lang3.Validate;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
|
||||||
import ca.uhn.fhir.model.api.IResource;
|
|
||||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
|
||||||
import ca.uhn.fhir.model.api.TagList;
|
|
||||||
import ca.uhn.fhir.parser.DataFormatException;
|
|
||||||
import ca.uhn.fhir.parser.IParser;
|
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
|
||||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
|
||||||
import ca.uhn.fhir.rest.param.ParameterUtil;
|
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
|
||||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
|
||||||
|
|
||||||
public class ResourceParameter implements IParameter {
|
public class ResourceParameter implements IParameter {
|
||||||
|
|
||||||
|
@ -193,11 +191,22 @@ public class ResourceParameter implements IParameter {
|
||||||
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
|
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
|
||||||
if (EncodingEnum.forContentTypeStrict(ct) == null) {
|
if (EncodingEnum.forContentTypeStrict(ct) == null) {
|
||||||
FhirContext ctx = theRequest.getServer().getFhirContext();
|
FhirContext ctx = theRequest.getServer().getFhirContext();
|
||||||
IBaseBinary binary = (IBaseBinary) ctx.getResourceDefinition("Binary").newInstance();
|
IBaseBinary binary = BinaryUtil.newBinary(ctx);
|
||||||
binary.setId(theRequest.getId());
|
binary.setId(theRequest.getId());
|
||||||
binary.setContentType(ct);
|
binary.setContentType(ct);
|
||||||
binary.setContent(theRequest.loadRequestContents());
|
binary.setContent(theRequest.loadRequestContents());
|
||||||
retVal = binary;
|
retVal = binary;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Security context header, which is only in
|
||||||
|
* DSTU3+
|
||||||
|
*/
|
||||||
|
if (ctx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
|
||||||
|
String securityContext = theRequest.getHeader(Constants.HEADER_X_SECURITY_CONTEXT);
|
||||||
|
if (isNotBlank(securityContext)) {
|
||||||
|
BinaryUtil.setSecurityContext(ctx, binary, securityContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,8 @@ import static org.mockito.Matchers.isNull;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class JsonParserDstu3Test {
|
public class JsonParserDstu3Test {
|
||||||
private static FhirContext ourCtx = FhirContext.forDstu3();
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserDstu3Test.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserDstu3Test.class);
|
||||||
|
private static FhirContext ourCtx = FhirContext.forDstu3();
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void after() {
|
public void after() {
|
||||||
|
@ -77,22 +77,6 @@ public class JsonParserDstu3Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See #720
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testParseCustomResourceType() {
|
|
||||||
String input = "{\"resourceType\":\"Bug720ResourceType\",\"meta\":{\"profile\":[\"http://example.com/StructureDefinition/dontuse#Bug720ResourceType\"]},\"supportedVersion\":\"2.5.x\",\"templatesConsentTemplate\":[{\"domainName\":\"name\",\"Name\":\"template_01\",\"version\":\"1.0\",\"title\":\"title\",\"comment\":\"comment\",\"contact\":{\"resourceType\":\"Person\",\"name\":[{\"family\":\"Mustermann\",\"given\":[\"Max\"]}],\"telecom\":[{\"system\":\"email\",\"value\":\"max.mustermann@mail.de\"},{\"system\":\"phone\",\"value\":\"+49 1234 23232\"}],\"address\":[{\"text\":\"street 1-2\",\"city\":\"city\",\"postalCode\":\"12345\",\"country\":\"Germany\"}]}}]}";
|
|
||||||
Bug720ResourceType parsed = ourCtx.newJsonParser().parseResource(Bug720ResourceType.class, input);
|
|
||||||
|
|
||||||
assertEquals(1, parsed.getTemplates().size());
|
|
||||||
assertEquals(Bug720Datatype.class, parsed.getTemplates().get(0).getClass());
|
|
||||||
assertEquals("Mustermann", ((Bug720Datatype)parsed.getTemplates().get(0)).getContact().getNameFirstRep().getFamily());
|
|
||||||
|
|
||||||
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(parsed));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See #563
|
* See #563
|
||||||
*/
|
*/
|
||||||
|
@ -110,6 +94,26 @@ public class JsonParserDstu3Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
|
||||||
|
String refVal = "http://my.org/FooBar";
|
||||||
|
|
||||||
|
Patient fhirPat = new Patient();
|
||||||
|
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
|
||||||
|
|
||||||
|
IParser parser = ourCtx.newJsonParser();
|
||||||
|
|
||||||
|
String output = parser.encodeResourceToString(fhirPat);
|
||||||
|
System.out.println("output: " + output);
|
||||||
|
|
||||||
|
// Deserialize then check that valueReference value is still correct
|
||||||
|
fhirPat = parser.parseResource(Patient.class, output);
|
||||||
|
|
||||||
|
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
|
||||||
|
Assert.assertEquals(1, extlst.size());
|
||||||
|
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See #544
|
* See #544
|
||||||
*/
|
*/
|
||||||
|
@ -506,6 +510,22 @@ public class JsonParserDstu3Test {
|
||||||
assertEquals("VERSION2", label.getVersion());
|
assertEquals("VERSION2", label.getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodeBinaryWithSecurityContext() {
|
||||||
|
Binary bin = new Binary();
|
||||||
|
bin.setContentType("text/plain");
|
||||||
|
bin.setContent("Now is the time".getBytes());
|
||||||
|
Reference securityContext = new Reference();
|
||||||
|
securityContext.setReference("DiagnosticReport/1");
|
||||||
|
bin.setSecurityContext(securityContext);
|
||||||
|
String encoded = ourCtx.newJsonParser().encodeResourceToString(bin);
|
||||||
|
ourLog.info(encoded);
|
||||||
|
assertThat(encoded, containsString("Binary"));
|
||||||
|
assertThat(encoded, containsString("\"contentType\":\"text/plain\""));
|
||||||
|
assertThat(encoded, containsString("\"content\":\"Tm93IGlzIHRoZSB0aW1l\""));
|
||||||
|
assertThat(encoded, containsString("\"securityContext\":{\"reference\":\"DiagnosticReport/1\"}"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncodeBundleNewBundleNoText() {
|
public void testEncodeBundleNewBundleNoText() {
|
||||||
|
|
||||||
|
@ -576,23 +596,6 @@ public class JsonParserDstu3Test {
|
||||||
assertEquals("{\"resourceType\":\"Binary\"}", output);
|
assertEquals("{\"resourceType\":\"Binary\"}", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testEncodeBinaryWithSecurityContext() {
|
|
||||||
Binary bin = new Binary();
|
|
||||||
bin.setContentType("text/plain");
|
|
||||||
bin.setContent("Now is the time".getBytes());
|
|
||||||
Reference securityContext = new Reference();
|
|
||||||
securityContext.setReference("DiagnosticReport/1");
|
|
||||||
bin.setSecurityContext(securityContext);
|
|
||||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(bin);
|
|
||||||
assertThat(encoded, containsString("Binary"));
|
|
||||||
assertThat(encoded, containsString("contentType"));
|
|
||||||
assertThat(encoded, containsString("text/plain"));
|
|
||||||
assertThat(encoded, containsString("Tm93IGlzIHRoZSB0aW1l"));
|
|
||||||
assertThat(encoded, containsString("securityContext"));
|
|
||||||
assertThat(encoded, containsString("{\"reference\":\"DiagnosticReport/1\"}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #158
|
* #158
|
||||||
*/
|
*/
|
||||||
|
@ -1865,7 +1868,7 @@ public class JsonParserDstu3Test {
|
||||||
Binary patient = new Binary();
|
Binary patient = new Binary();
|
||||||
patient.setId(new IdType("http://base/Binary/11/_history/22"));
|
patient.setId(new IdType("http://base/Binary/11/_history/22"));
|
||||||
patient.setContentType("foo");
|
patient.setContentType("foo");
|
||||||
patient.setContent(new byte[] { 1, 2, 3, 4 });
|
patient.setContent(new byte[]{1, 2, 3, 4});
|
||||||
|
|
||||||
String val = ourCtx.newJsonParser().encodeResourceToString(patient);
|
String val = ourCtx.newJsonParser().encodeResourceToString(patient);
|
||||||
|
|
||||||
|
@ -1927,6 +1930,21 @@ public class JsonParserDstu3Test {
|
||||||
assertEquals("patient family", p.getName().get(0).getFamilyElement().getValue());
|
assertEquals("patient family", p.getName().get(0).getFamilyElement().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See #720
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testParseCustomResourceType() {
|
||||||
|
String input = "{\"resourceType\":\"Bug720ResourceType\",\"meta\":{\"profile\":[\"http://example.com/StructureDefinition/dontuse#Bug720ResourceType\"]},\"supportedVersion\":\"2.5.x\",\"templatesConsentTemplate\":[{\"domainName\":\"name\",\"Name\":\"template_01\",\"version\":\"1.0\",\"title\":\"title\",\"comment\":\"comment\",\"contact\":{\"resourceType\":\"Person\",\"name\":[{\"family\":\"Mustermann\",\"given\":[\"Max\"]}],\"telecom\":[{\"system\":\"email\",\"value\":\"max.mustermann@mail.de\"},{\"system\":\"phone\",\"value\":\"+49 1234 23232\"}],\"address\":[{\"text\":\"street 1-2\",\"city\":\"city\",\"postalCode\":\"12345\",\"country\":\"Germany\"}]}}]}";
|
||||||
|
Bug720ResourceType parsed = ourCtx.newJsonParser().parseResource(Bug720ResourceType.class, input);
|
||||||
|
|
||||||
|
assertEquals(1, parsed.getTemplates().size());
|
||||||
|
assertEquals(Bug720Datatype.class, parsed.getTemplates().get(0).getClass());
|
||||||
|
assertEquals("Mustermann", ((Bug720Datatype) parsed.getTemplates().get(0)).getContact().getNameFirstRep().getFamily());
|
||||||
|
|
||||||
|
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #480
|
* #480
|
||||||
*/
|
*/
|
||||||
|
@ -2182,39 +2200,6 @@ public class JsonParserDstu3Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* See #344
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testParserIsCaseSensitive() {
|
|
||||||
Observation obs = new Observation();
|
|
||||||
SampledData data = new SampledData();
|
|
||||||
data.setData("1 2 3");
|
|
||||||
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
|
|
||||||
data.setPeriod(1000L);
|
|
||||||
obs.setValue(data);
|
|
||||||
|
|
||||||
IParser p = ourCtx.newJsonParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
|
|
||||||
String encoded = p.encodeResourceToString(obs);
|
|
||||||
ourLog.info(encoded);
|
|
||||||
|
|
||||||
p.parseResource(encoded);
|
|
||||||
|
|
||||||
try {
|
|
||||||
p.parseResource(encoded.replace("Observation", "observation"));
|
|
||||||
fail();
|
|
||||||
} catch (DataFormatException e) {
|
|
||||||
assertEquals("Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
|
|
||||||
fail();
|
|
||||||
} catch (DataFormatException e) {
|
|
||||||
assertEquals("Unknown element 'valueSampleddata' found during parse", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseWithPrecision() {
|
public void testParseWithPrecision() {
|
||||||
String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}";
|
String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}";
|
||||||
|
@ -2252,6 +2237,39 @@ public class JsonParserDstu3Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See #344
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testParserIsCaseSensitive() {
|
||||||
|
Observation obs = new Observation();
|
||||||
|
SampledData data = new SampledData();
|
||||||
|
data.setData("1 2 3");
|
||||||
|
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
|
||||||
|
data.setPeriod(1000L);
|
||||||
|
obs.setValue(data);
|
||||||
|
|
||||||
|
IParser p = ourCtx.newJsonParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
|
||||||
|
String encoded = p.encodeResourceToString(obs);
|
||||||
|
ourLog.info(encoded);
|
||||||
|
|
||||||
|
p.parseResource(encoded);
|
||||||
|
|
||||||
|
try {
|
||||||
|
p.parseResource(encoded.replace("Observation", "observation"));
|
||||||
|
fail();
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
assertEquals("Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
|
||||||
|
fail();
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
assertEquals("Unknown element 'valueSampleddata' found during parse", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See #144 and #146
|
* See #144 and #146
|
||||||
*/
|
*/
|
||||||
|
@ -2361,26 +2379,6 @@ public class JsonParserDstu3Test {
|
||||||
assertTrue(result.isSuccessful());
|
assertTrue(result.isSuccessful());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
|
|
||||||
String refVal = "http://my.org/FooBar";
|
|
||||||
|
|
||||||
Patient fhirPat = new Patient();
|
|
||||||
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
|
|
||||||
|
|
||||||
IParser parser = ourCtx.newJsonParser();
|
|
||||||
|
|
||||||
String output = parser.encodeResourceToString(fhirPat);
|
|
||||||
System.out.println("output: " + output);
|
|
||||||
|
|
||||||
// Deserialize then check that valueReference value is still correct
|
|
||||||
fhirPat = parser.parseResource(Patient.class, output);
|
|
||||||
|
|
||||||
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
|
|
||||||
Assert.assertEquals(1, extlst.size());
|
|
||||||
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void afterClassClearContext() {
|
public static void afterClassClearContext() {
|
||||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
|
|
@ -82,6 +82,23 @@ public class XmlParserDstu3Test {
|
||||||
ourCtx.setNarrativeGenerator(null);
|
ourCtx.setNarrativeGenerator(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodeBinaryWithSecurityContext() {
|
||||||
|
Binary bin = new Binary();
|
||||||
|
bin.setContentType("text/plain");
|
||||||
|
bin.setContent("Now is the time".getBytes());
|
||||||
|
Reference securityContext = new Reference();
|
||||||
|
securityContext.setReference("DiagnosticReport/1");
|
||||||
|
bin.setSecurityContext(securityContext);
|
||||||
|
String encoded = ourCtx.newXmlParser().encodeResourceToString(bin);
|
||||||
|
ourLog.info(encoded);
|
||||||
|
assertThat(encoded, containsString("Binary"));
|
||||||
|
assertThat(encoded, containsString("<contentType value=\"text/plain\"/>"));
|
||||||
|
assertThat(encoded, containsString("<securityContext><reference value=\"DiagnosticReport/1\"/></securityContext>"));
|
||||||
|
assertThat(encoded, containsString("<content value=\"Tm93IGlzIHRoZSB0aW1l\"/>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See #544
|
* See #544
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
package ca.uhn.fhir.rest.server;
|
package ca.uhn.fhir.rest.server;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import static org.junit.Assert.assertEquals;
|
import ca.uhn.fhir.rest.annotation.*;
|
||||||
import static org.junit.Assert.assertNull;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import java.util.concurrent.TimeUnit;
|
import ca.uhn.fhir.util.PortUtil;
|
||||||
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.client.methods.HttpPut;
|
||||||
import org.apache.http.entity.ByteArrayEntity;
|
import org.apache.http.entity.ByteArrayEntity;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
@ -18,18 +20,20 @@ import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.servlet.ServletHandler;
|
import org.eclipse.jetty.servlet.ServletHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r4.model.*;
|
import org.hl7.fhir.r4.model.Binary;
|
||||||
import org.junit.*;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import java.util.concurrent.TimeUnit;
|
||||||
import ca.uhn.fhir.rest.annotation.Create;
|
|
||||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
|
||||||
import ca.uhn.fhir.util.PortUtil;
|
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
|
||||||
|
|
||||||
public class CreateBinaryR4Test {
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class BinaryServerR4Test {
|
||||||
private static CloseableHttpClient ourClient;
|
private static CloseableHttpClient ourClient;
|
||||||
private static FhirContext ourCtx = FhirContext.forR4();
|
private static FhirContext ourCtx = FhirContext.forR4();
|
||||||
private static Binary ourLastBinary;
|
private static Binary ourLastBinary;
|
||||||
|
@ -37,24 +41,73 @@ public class CreateBinaryR4Test {
|
||||||
private static String ourLastBinaryString;
|
private static String ourLastBinaryString;
|
||||||
private static int ourPort;
|
private static int ourPort;
|
||||||
private static Server ourServer;
|
private static Server ourServer;
|
||||||
|
private static IdType ourLastId;
|
||||||
|
private static Binary ourNextBinary;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
ourLastBinary = null;
|
ourLastBinary = null;
|
||||||
ourLastBinaryBytes = null;
|
ourLastBinaryBytes = null;
|
||||||
ourLastBinaryString = null;
|
ourLastBinaryString = null;
|
||||||
|
ourLastId = null;
|
||||||
|
ourNextBinary = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRawBytesBinaryContentType() throws Exception {
|
public void testGetWithNoAccept() throws Exception {
|
||||||
|
|
||||||
|
ourNextBinary = new Binary();
|
||||||
|
ourNextBinary.setId("Binary/A/_history/222");
|
||||||
|
ourNextBinary.setContent(new byte[]{0, 1, 2, 3, 4});
|
||||||
|
ourNextBinary.setSecurityContext(new Reference("Patient/1"));
|
||||||
|
ourNextBinary.setContentType("application/foo");
|
||||||
|
|
||||||
|
HttpGet get = new HttpGet("http://localhost:" + ourPort + "/Binary/A");
|
||||||
|
get.addHeader("Content-Type", "application/foo");
|
||||||
|
CloseableHttpResponse status = ourClient.execute(get);
|
||||||
|
try {
|
||||||
|
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||||
|
assertEquals("application/foo", status.getEntity().getContentType().getValue());
|
||||||
|
assertEquals("Patient/1", status.getFirstHeader(Constants.HEADER_X_SECURITY_CONTEXT).getValue());
|
||||||
|
assertEquals("W/\"222\"", status.getFirstHeader(Constants.HEADER_ETAG).getValue());
|
||||||
|
assertEquals("http://localhost:" + ourPort + "/Binary/A/_history/222", status.getFirstHeader(Constants.HEADER_LOCATION).getValue());
|
||||||
|
|
||||||
|
byte[] content = IOUtils.toByteArray(status.getEntity().getContent());
|
||||||
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, content);
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostBinaryWithSecurityContext() throws Exception {
|
||||||
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
||||||
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 }));
|
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
|
||||||
|
post.addHeader("Content-Type", "application/foo");
|
||||||
|
post.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, "Encounter/2");
|
||||||
|
CloseableHttpResponse status = ourClient.execute(post);
|
||||||
|
try {
|
||||||
|
assertNull(ourLastId);
|
||||||
|
assertEquals("application/foo", ourLastBinary.getContentType());
|
||||||
|
assertEquals("Encounter/2", ourLastBinary.getSecurityContext().getReference());
|
||||||
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
|
||||||
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostRawBytesBinaryContentType() throws Exception {
|
||||||
|
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
||||||
|
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
|
||||||
post.addHeader("Content-Type", "application/foo");
|
post.addHeader("Content-Type", "application/foo");
|
||||||
CloseableHttpResponse status = ourClient.execute(post);
|
CloseableHttpResponse status = ourClient.execute(post);
|
||||||
try {
|
try {
|
||||||
|
assertNull(ourLastId);
|
||||||
assertEquals("application/foo", ourLastBinary.getContentType());
|
assertEquals("application/foo", ourLastBinary.getContentType());
|
||||||
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
|
||||||
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinaryBytes);
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(status);
|
IOUtils.closeQuietly(status);
|
||||||
}
|
}
|
||||||
|
@ -64,11 +117,11 @@ public class CreateBinaryR4Test {
|
||||||
* Technically the client shouldn't be doing it this way, but we'll be accepting
|
* Technically the client shouldn't be doing it this way, but we'll be accepting
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testRawBytesFhirContentType() throws Exception {
|
public void testPostRawBytesFhirContentType() throws Exception {
|
||||||
|
|
||||||
Binary b = new Binary();
|
Binary b = new Binary();
|
||||||
b.setContentType("application/foo");
|
b.setContentType("application/foo");
|
||||||
b.setContent(new byte[] { 0, 1, 2, 3, 4 });
|
b.setContent(new byte[]{0, 1, 2, 3, 4});
|
||||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(b);
|
String encoded = ourCtx.newJsonParser().encodeResourceToString(b);
|
||||||
|
|
||||||
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
||||||
|
@ -77,14 +130,14 @@ public class CreateBinaryR4Test {
|
||||||
CloseableHttpResponse status = ourClient.execute(post);
|
CloseableHttpResponse status = ourClient.execute(post);
|
||||||
try {
|
try {
|
||||||
assertEquals("application/foo", ourLastBinary.getContentType());
|
assertEquals("application/foo", ourLastBinary.getContentType());
|
||||||
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(status);
|
IOUtils.closeQuietly(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRawBytesFhirContentTypeContainingFhir() throws Exception {
|
public void testPostRawBytesFhirContentTypeContainingFhir() throws Exception {
|
||||||
|
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.getText().setDivAsString("A PATIENT");
|
p.getText().setDivAsString("A PATIENT");
|
||||||
|
@ -109,13 +162,32 @@ public class CreateBinaryR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRawBytesNoContentType() throws Exception {
|
public void testPostRawBytesNoContentType() throws Exception {
|
||||||
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
|
||||||
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 }));
|
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
|
||||||
CloseableHttpResponse status = ourClient.execute(post);
|
CloseableHttpResponse status = ourClient.execute(post);
|
||||||
try {
|
try {
|
||||||
assertNull(ourLastBinary.getContentType());
|
assertNull(ourLastBinary.getContentType());
|
||||||
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPutBinaryWithSecurityContext() throws Exception {
|
||||||
|
HttpPut post = new HttpPut("http://localhost:" + ourPort + "/Binary/A");
|
||||||
|
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
|
||||||
|
post.addHeader("Content-Type", "application/foo");
|
||||||
|
post.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, "Encounter/2");
|
||||||
|
CloseableHttpResponse status = ourClient.execute(post);
|
||||||
|
try {
|
||||||
|
assertEquals("Binary/A", ourLastId.getValue());
|
||||||
|
assertEquals("Binary/A", ourLastBinary.getId());
|
||||||
|
assertEquals("application/foo", ourLastBinary.getContentType());
|
||||||
|
assertEquals("Encounter/2", ourLastBinary.getSecurityContext().getReference());
|
||||||
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
|
||||||
|
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(status);
|
IOUtils.closeQuietly(status);
|
||||||
}
|
}
|
||||||
|
@ -149,7 +221,6 @@ public class CreateBinaryR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BinaryProvider implements IResourceProvider {
|
public static class BinaryProvider implements IResourceProvider {
|
||||||
|
|
||||||
@Create()
|
@Create()
|
||||||
public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
|
public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
|
||||||
ourLastBinary = theBinary;
|
ourLastBinary = theBinary;
|
||||||
|
@ -163,6 +234,20 @@ public class CreateBinaryR4Test {
|
||||||
return Binary.class;
|
return Binary.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Read
|
||||||
|
public Binary read(@IdParam IdType theId) {
|
||||||
|
return ourNextBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Update()
|
||||||
|
public MethodOutcome updateBinary(@IdParam IdType theId, @ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
|
||||||
|
ourLastId = theId;
|
||||||
|
ourLastBinary = theBinary;
|
||||||
|
ourLastBinaryString = theBinaryString;
|
||||||
|
ourLastBinaryBytes = theBinaryBytes;
|
||||||
|
return new MethodOutcome(new IdType("Binary/001/_history/002"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -175,6 +175,11 @@
|
||||||
was not encoded correctly. Thanks to Malcolm McRoberts for the pull
|
was not encoded correctly. Thanks to Malcolm McRoberts for the pull
|
||||||
request with fix and test case!
|
request with fix and test case!
|
||||||
</action>
|
</action>
|
||||||
|
<action type="add">
|
||||||
|
The Binary resource endpoint now supports the `X-Security-Context` header when
|
||||||
|
reading or writing Binary contents using their native Content-Type (i.e exchanging
|
||||||
|
the raw binary with the server, as opposed to exchanging a FHIR resource).
|
||||||
|
</action>
|
||||||
</release>
|
</release>
|
||||||
<release version="3.0.0" date="2017-09-27">
|
<release version="3.0.0" date="2017-09-27">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
Loading…
Reference in New Issue