Add support for Binary X-Security-Context header in server

This commit is contained in:
James Agnew 2017-11-02 11:38:43 -04:00
parent 256d541dda
commit 75bfb6af1b
16 changed files with 1058 additions and 843 deletions

View File

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

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

@ -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();
}
}
}
} }
} }

View File

@ -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);

View File

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

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,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;
}
} }

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

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

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

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

View File

@ -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) {

View File

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

View File

@ -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();

View File

@ -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
*/ */

View File

@ -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"));
}
} }
} }

View File

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