Fix #65 - Correctly parse and encode extensions on non-repeatable

primitive fields
This commit is contained in:
James Agnew 2014-12-23 14:10:53 -05:00
parent 81f9e492f3
commit 81851f4808
30 changed files with 1040 additions and 126 deletions

View File

@ -22,9 +22,12 @@ package ca.uhn.fhir.model.api;
import java.io.InputStream; import java.io.InputStream;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
public interface IFhirVersion { public interface IFhirVersion {
@ -37,7 +40,7 @@ public interface IFhirVersion {
IResource generateProfile(RuntimeResourceDefinition theRuntimeResourceDefinition, String theServerBase); IResource generateProfile(RuntimeResourceDefinition theRuntimeResourceDefinition, String theServerBase);
Object createServerConformanceProvider(RestfulServer theRestfulServer); IServerConformanceProvider<? extends IBaseResource> createServerConformanceProvider(RestfulServer theRestfulServer);
String getPathToSchemaDefinitions(); String getPathToSchemaDefinitions();

View File

@ -288,20 +288,20 @@ public class JsonParser extends BaseParser implements IParser {
writeTagWithTextNode(theEventWriter, "instant", nextEntry.getDeletedAt()); writeTagWithTextNode(theEventWriter, "instant", nextEntry.getDeletedAt());
} }
// linkStarted = false; // linkStarted = false;
// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "self", nextEntry.getLinkSelf(), linkStarted); // linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "self", nextEntry.getLinkSelf(), linkStarted);
// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "alternate", nextEntry.getLinkAlternate(), linkStarted); // linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "alternate", nextEntry.getLinkAlternate(), linkStarted);
// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "search", nextEntry.getLinkSearch(), linkStarted); // linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "search", nextEntry.getLinkSearch(), linkStarted);
// if (linkStarted) { // if (linkStarted) {
// theEventWriter.writeEnd(); // theEventWriter.writeEnd();
// } // }
// //
// writeOptionalTagWithTextNode(theEventWriter, "updated", nextEntry.getUpdated()); // writeOptionalTagWithTextNode(theEventWriter, "updated", nextEntry.getUpdated());
// writeOptionalTagWithTextNode(theEventWriter, "published", nextEntry.getPublished()); // writeOptionalTagWithTextNode(theEventWriter, "published", nextEntry.getPublished());
// //
// writeCategories(theEventWriter, nextEntry.getCategories()); // writeCategories(theEventWriter, nextEntry.getCategories());
// //
// writeAuthor(nextEntry, theEventWriter); // writeAuthor(nextEntry, theEventWriter);
IResource resource = nextEntry.getResource(); IResource resource = nextEntry.getResource();
if (resource != null && !resource.isEmpty() && !deleted) { if (resource != null && !resource.isEmpty() && !deleted) {
@ -532,7 +532,12 @@ public class JsonParser extends BaseParser implements IParser {
} }
if (extensions.size() > 0 || modifierExtensions.size() > 0) { if (extensions.size() > 0 || modifierExtensions.size() > 0) {
theEventWriter.writeStartArray('_' + currentChildName); if (inArray) {
// If this is a repeatable field, the extensions go in an array too
theEventWriter.writeStartArray('_' + currentChildName);
} else {
theEventWriter.writeStartObject('_' + currentChildName);
}
for (int i = 0; i < valueIdx; i++) { for (int i = 0; i < valueIdx; i++) {
boolean haveContent = false; boolean haveContent = false;
@ -552,9 +557,13 @@ public class JsonParser extends BaseParser implements IParser {
if (!haveContent) { if (!haveContent) {
theEventWriter.writeNull(); theEventWriter.writeNull();
} else { } else {
theEventWriter.writeStartObject(); if (inArray) {
theEventWriter.writeStartObject();
}
writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, heldExts, heldModExts, null); writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, heldExts, heldModExts, null);
theEventWriter.writeEnd(); if (inArray) {
theEventWriter.writeEnd();
}
} }
} }
@ -735,11 +744,23 @@ public class JsonParser extends BaseParser implements IParser {
} }
} }
private void parseAlternates(JsonValue theAlternateVal, ParserState<?> theState) { private void parseAlternates(JsonValue theAlternateVal, ParserState<?> theState, String theElementName) {
if (theAlternateVal == null || theAlternateVal.getValueType() == ValueType.NULL) { if (theAlternateVal == null || theAlternateVal.getValueType() == ValueType.NULL) {
return; return;
} }
if (theAlternateVal instanceof JsonArray) {
JsonArray array = (JsonArray) theAlternateVal;
if (array.size() > 1) {
throw new DataFormatException("Unexpected array of length " + array.size() + " (expected 0 or 1) for element: " + theElementName);
}
if (array.size() == 0) {
return;
}
parseAlternates(array.getJsonObject(0), theState, theElementName);
return;
}
boolean newerThanDstu1 = myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1); boolean newerThanDstu1 = myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1);
JsonObject alternate = (JsonObject) theAlternateVal; JsonObject alternate = (JsonObject) theAlternateVal;
for (Entry<String, JsonValue> nextEntry : alternate.entrySet()) { for (Entry<String, JsonValue> nextEntry : alternate.entrySet()) {
@ -880,7 +901,7 @@ public class JsonParser extends BaseParser implements IParser {
} }
JsonValue nextVal = theObject.get(nextName); JsonValue nextVal = theObject.get(nextName);
parseChildren(theState, nextName, nextVal, null); parseChildren(theState, nextName, nextVal, null, null);
} }
} }
@ -926,9 +947,10 @@ public class JsonParser extends BaseParser implements IParser {
} }
JsonValue nextVal = theObject.get(nextName); JsonValue nextVal = theObject.get(nextName);
JsonValue alternateVal = theObject.get('_' + nextName); String alternateName = '_' + nextName;
JsonValue alternateVal = theObject.get(alternateName);
parseChildren(theState, nextName, nextVal, alternateVal); parseChildren(theState, nextName, nextVal, alternateVal, alternateName);
} }
@ -942,7 +964,7 @@ public class JsonParser extends BaseParser implements IParser {
} }
} }
private void parseChildren(ParserState<?> theState, String theName, JsonValue theJsonVal, JsonValue theAlternateVal) { private void parseChildren(ParserState<?> theState, String theName, JsonValue theJsonVal, JsonValue theAlternateVal, String theAlternateName) {
switch (theJsonVal.getValueType()) { switch (theJsonVal.getValueType()) {
case ARRAY: { case ARRAY: {
JsonArray nextArray = (JsonArray) theJsonVal; JsonArray nextArray = (JsonArray) theJsonVal;
@ -953,13 +975,13 @@ public class JsonParser extends BaseParser implements IParser {
if (nextAlternateArray != null) { if (nextAlternateArray != null) {
nextAlternate = nextAlternateArray.get(i); nextAlternate = nextAlternateArray.get(i);
} }
parseChildren(theState, theName, nextObject, nextAlternate); parseChildren(theState, theName, nextObject, nextAlternate, theAlternateName);
} }
break; break;
} }
case OBJECT: { case OBJECT: {
theState.enteringNewElement(null, theName); theState.enteringNewElement(null, theName);
parseAlternates(theAlternateVal, theState); parseAlternates(theAlternateVal, theState, theAlternateName);
JsonObject nextObject = (JsonObject) theJsonVal; JsonObject nextObject = (JsonObject) theJsonVal;
boolean preResource = false; boolean preResource = false;
if (theState.isPreResource()) { if (theState.isPreResource()) {
@ -981,7 +1003,7 @@ public class JsonParser extends BaseParser implements IParser {
JsonString nextValStr = (JsonString) theJsonVal; JsonString nextValStr = (JsonString) theJsonVal;
theState.enteringNewElement(null, theName); theState.enteringNewElement(null, theName);
theState.attributeValue("value", nextValStr.getString()); theState.attributeValue("value", nextValStr.getString());
parseAlternates(theAlternateVal, theState); parseAlternates(theAlternateVal, theState, theAlternateName);
theState.endingElement(); theState.endingElement();
break; break;
} }
@ -990,7 +1012,7 @@ public class JsonParser extends BaseParser implements IParser {
case TRUE: case TRUE:
theState.enteringNewElement(null, theName); theState.enteringNewElement(null, theName);
theState.attributeValue("value", theJsonVal.toString()); theState.attributeValue("value", theJsonVal.toString());
parseAlternates(theAlternateVal, theState); parseAlternates(theAlternateVal, theState, theAlternateName);
theState.endingElement(); theState.endingElement();
break; break;
case NULL: case NULL:
@ -1015,7 +1037,7 @@ public class JsonParser extends BaseParser implements IParser {
parseExtension(theState, jsonVal, true); parseExtension(theState, jsonVal, true);
} else { } else {
JsonValue jsonVal = nextExtObj.get(next); JsonValue jsonVal = nextExtObj.get(next);
parseChildren(theState, next, jsonVal, null); parseChildren(theState, next, jsonVal, null, null);
} }
} }
theState.endingElement(); theState.endingElement();
@ -1039,7 +1061,7 @@ public class JsonParser extends BaseParser implements IParser {
JsonArray arrayValue = (JsonArray) jsonVal; JsonArray arrayValue = (JsonArray) jsonVal;
parseExtensionInDstu2Style(theModifier, theState, extUrl, nextKey, arrayValue); parseExtensionInDstu2Style(theModifier, theState, extUrl, nextKey, arrayValue);
} else { } else {
parseChildren(theState, nextKey, jsonVal, null); parseChildren(theState, nextKey, jsonVal, null, null);
} }
} }
} }
@ -1371,9 +1393,9 @@ public class JsonParser extends BaseParser implements IParser {
boolean noValue = value == null || value.isEmpty(); boolean noValue = value == null || value.isEmpty();
if (noValue && ext.getAllUndeclaredExtensions().isEmpty()) { if (noValue && ext.getAllUndeclaredExtensions().isEmpty()) {
ourLog.debug("Extension with URL[{}] has no value", extensionUrl); ourLog.debug("Extension with URL[{}] has no value", extensionUrl);
} else if (noValue) { } else if (noValue) {
BaseRuntimeElementDefinition<?> elemDef = null; BaseRuntimeElementDefinition<?> elemDef = null;
@ -1381,7 +1403,7 @@ public class JsonParser extends BaseParser implements IParser {
extractAndWriteExtensionsAsDirectChild(ext, theEventWriter, elemDef, resDef, theResource, extensionUrl); extractAndWriteExtensionsAsDirectChild(ext, theEventWriter, elemDef, resDef, theResource, extensionUrl);
} else { } else {
RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition(); RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
String childName = extDef.getChildNameByDatatype(value.getClass()); String childName = extDef.getChildNameByDatatype(value.getClass());
BaseRuntimeElementDefinition<?> childDef; BaseRuntimeElementDefinition<?> childDef;
@ -1396,7 +1418,7 @@ public class JsonParser extends BaseParser implements IParser {
childDef = extDef.getChildElementDefinitionByDatatype(value.getClass()); childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
} }
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true);
} }
theEventWriter.writeEnd(); theEventWriter.writeEnd();

View File

@ -815,21 +815,36 @@ public class XmlParser extends BaseParser implements IParser {
theEventWriter.writeEndElement(); theEventWriter.writeEndElement();
} }
private void encodeUndeclaredExtensions(RuntimeResourceDefinition theResDef, IBaseResource theResource, XMLStreamWriter theWriter, List<ExtensionDt> extensions, String tagName, private void encodeUndeclaredExtensions(RuntimeResourceDefinition theResDef, IBaseResource theResource, XMLStreamWriter theWriter, List<ExtensionDt> theExtensions, String tagName,
boolean theIncludedResource) throws XMLStreamException, DataFormatException { boolean theIncludedResource) throws XMLStreamException, DataFormatException {
for (ExtensionDt next : extensions) { for (ExtensionDt next : theExtensions) {
theWriter.writeStartElement(tagName); theWriter.writeStartElement(tagName);
theWriter.writeAttribute("url", next.getUrl().getValue()); theWriter.writeAttribute("url", next.getUrl().getValue());
if (next.getValue() != null) { if (next.getValue() != null) {
IElement nextValue = next.getValue(); IElement value = next.getValue();
// RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
// String childName = extDef.getChildNameByDatatype(nextValue.getClass());
// if (childName == null) {
// throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + nextValue.getClass().getCanonicalName());
// }
// BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(nextValue.getClass());
//
//
RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition(); RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
String childName = extDef.getChildNameByDatatype(nextValue.getClass()); String childName = extDef.getChildNameByDatatype(value.getClass());
BaseRuntimeElementDefinition<?> childDef;
if (childName == null) { if (childName == null) {
throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + nextValue.getClass().getCanonicalName()); childDef = myContext.getElementDefinition(value.getClass());
if (childDef == null) {
throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName());
} else {
childName = RuntimeChildUndeclaredExtensionDefinition.createExtensionChildName(childDef);
}
} else {
childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
} }
BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(nextValue.getClass()); encodeChildElementToStreamWriter(theResDef, theResource, theWriter, value, childName, childDef, null, theIncludedResource);
encodeChildElementToStreamWriter(theResDef, theResource, theWriter, nextValue, childName, childDef, null, theIncludedResource);
} }
// child extensions // child extensions

View File

@ -0,0 +1,16 @@
package ca.uhn.fhir.rest.server;
import javax.servlet.http.HttpServletRequest;
import org.hl7.fhir.instance.model.IBaseResource;
public interface IServerConformanceProvider<T extends IBaseResource> {
/**
* Actually create and return the conformance statement
*
* See the class documentation for an important note if you are extending this class
*/
public abstract T getServerConformance(HttpServletRequest theRequest);
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.parser;
import static org.junit.Assert.assertThat;
import org.hamcrest.core.StringContains;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.composite.QuantityDt;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
public class MultiVersionXmlParserTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MultiVersionXmlParserTest.class);
@Test
public void testEncodeExtensionFromDifferentVersion() {
Patient p = new Patient();
p.addIdentifier("urn:sys", "001");
p.addUndeclaredExtension(false, "http://foo#ext", new QuantityDt(QuantityCompararatorEnum.LESSTHAN, 2.2, "g/L"));
String str;
str = FhirContext.forDstu1().newXmlParser().encodeResourceToString(p);
ourLog.info(str);
assertThat(str,StringContains.containsString("<extension url=\"http://foo#ext\"><valueQuantity><value value=\"2.2\"/><comparator value=\"&lt;\"/><units value=\"g/L\"/></valueQuantity></extension>"));
str = FhirContext.forDev().newXmlParser().encodeResourceToString(p);
ourLog.info(str);
assertThat(str,StringContains.containsString("<extension url=\"http://foo#ext\"><valueQuantity><value value=\"2.2\"/><comparator value=\"&lt;\"/><units value=\"g/L\"/></valueQuantity></extension>"));
}
}

View File

@ -26,5 +26,15 @@
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="target/classes" path="target/generated-sources/tinder">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="target/generated-resources/tinder">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View File

@ -2,5 +2,7 @@
<wb-module deploy-name="hapi-fhir-jpaserver-base"> <wb-module deploy-name="hapi-fhir-jpaserver-base">
<wb-resource deploy-path="/" source-path="/src/main/java"/> <wb-resource deploy-path="/" source-path="/src/main/java"/>
<wb-resource deploy-path="/" source-path="/src/main/resources"/> <wb-resource deploy-path="/" source-path="/src/main/resources"/>
<wb-resource deploy-path="/" source-path="/target/generated-sources/tinder"/>
<wb-resource deploy-path="/" source-path="/target/generated-resources/tinder"/>
</wb-module> </wb-module>
</project-modules> </project-modules>

View File

@ -0,0 +1,81 @@
package ca.uhn.fhir.jpa.provider;
import java.util.List;
import java.util.Map;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.model.dev.resource.Conformance;
import ca.uhn.fhir.model.dev.resource.Conformance.Rest;
import ca.uhn.fhir.model.dev.resource.Conformance.RestResource;
import ca.uhn.fhir.model.dev.resource.Conformance.RestResourceSearchParam;
import ca.uhn.fhir.model.dev.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dev.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.provider.dev.ServerConformanceProvider;
import ca.uhn.fhir.util.ExtensionConstants;
import javax.servlet.http.HttpServletRequest;
public class JpaConformanceProviderDev extends ServerConformanceProvider {
private String myImplementationDescription;
private IFhirSystemDao mySystemDao;
private volatile Conformance myCachedValue;
private RestfulServer myRestfulServer;
public JpaConformanceProviderDev(RestfulServer theRestfulServer, IFhirSystemDao theSystemDao) {
super(theRestfulServer);
myRestfulServer = theRestfulServer;
mySystemDao = theSystemDao;
super.setCache(false);
}
@Override
public Conformance getServerConformance(HttpServletRequest theRequest) {
Conformance retVal = myCachedValue;
Map<String, Long> counts = mySystemDao.getResourceCounts();
FhirContext ctx = myRestfulServer.getFhirContext();
retVal = super.getServerConformance(theRequest);
for (Rest nextRest : retVal.getRest()) {
for (RestResource nextResource : nextRest.getResource()) {
// Add resource counts
Long count = counts.get(nextResource.getTypeElement().getValueAsString());
if (count != null) {
nextResource.addUndeclaredExtension(false, ExtensionConstants.CONF_RESOURCE_COUNT, new DecimalDt(count));
}
// Add chained params
for (RestResourceSearchParam nextParam : nextResource.getSearchParam()) {
if (nextParam.getTypeElement().getValueAsEnum() == SearchParamTypeEnum.REFERENCE) {
List<BoundCodeDt<ResourceTypeEnum>> targets = nextParam.getTarget();
for (BoundCodeDt<ResourceTypeEnum> next : targets) {
RuntimeResourceDefinition def = ctx.getResourceDefinition(next.getValue());
for (RuntimeSearchParam nextChainedParam : def.getSearchParams()) {
nextParam.addChain(nextChainedParam.getName());
}
}
}
}
}
}
retVal.getImplementation().setDescription(myImplementationDescription);
myCachedValue = retVal;
return retVal;
}
public void setImplementationDescription(String theImplDesc) {
myImplementationDescription = theImplDesc;
}
}

View File

@ -21,14 +21,14 @@ import ca.uhn.fhir.util.ExtensionConstants;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
public class JpaConformanceProvider extends ServerConformanceProvider { public class JpaConformanceProviderDstu1 extends ServerConformanceProvider {
private String myImplementationDescription; private String myImplementationDescription;
private IFhirSystemDao mySystemDao; private IFhirSystemDao mySystemDao;
private volatile Conformance myCachedValue; private volatile Conformance myCachedValue;
private RestfulServer myRestfulServer; private RestfulServer myRestfulServer;
public JpaConformanceProvider(RestfulServer theRestfulServer, IFhirSystemDao theSystemDao) { public JpaConformanceProviderDstu1(RestfulServer theRestfulServer, IFhirSystemDao theSystemDao) {
super(theRestfulServer); super(theRestfulServer);
myRestfulServer = theRestfulServer; myRestfulServer = theRestfulServer;
mySystemDao = theSystemDao; mySystemDao = theSystemDao;

View File

@ -41,14 +41,14 @@
<dependency> <dependency>
<groupId>com.phloc</groupId> <groupId>com.phloc</groupId>
<artifactId>phloc-schematron</artifactId> <artifactId>phloc-schematron</artifactId>
<version>${phloc_schematron_version}</version> <version>${phloc_schematron_version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.phloc</groupId> <groupId>com.phloc</groupId>
<artifactId>phloc-commons</artifactId> <artifactId>phloc-commons</artifactId>
<version>${phloc_commons_version}</version> <version>${phloc_commons_version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test</artifactId> <artifactId>hapi-fhir-jpaserver-test</artifactId>
@ -151,11 +151,18 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>commons-dbcp</groupId> <groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId> <artifactId>commons-dbcp</artifactId>
<version>1.4</version> <version>1.4</version>
</dependency> </dependency>
-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Only required for CORS support --> <!-- Only required for CORS support -->
<dependency> <dependency>

View File

@ -10,7 +10,8 @@ import org.springframework.web.context.ContextLoaderListener;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.provider.JpaConformanceProvider; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDev;
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu1;
import ca.uhn.fhir.jpa.provider.JpaSystemProvider; import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
@ -22,103 +23,96 @@ import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
public class TestRestfulServer extends RestfulServer { public class TestRestfulServer extends RestfulServer {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestRestfulServer.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestRestfulServer.class);
private ApplicationContext myAppCtx; private ApplicationContext myAppCtx;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
protected void initialize() throws ServletException { protected void initialize() throws ServletException {
super.initialize(); super.initialize();
// try {
// ourLog.info("Creating database");
// DriverManager.getConnection("jdbc:derby:directory:" + System.getProperty("fhir.db.location") + ";create=true");
// } catch (Exception e) {
// ourLog.error("Failed to create database: {}",e);
// }
// myAppCtx = new ClassPathXmlApplicationContext("fhir-spring-uhnfhirtest-config.xml", "hapi-jpaserver-springbeans.xml");
// myAppCtx = new FileSystemXmlApplicationContext( // Get the spring context from the web container (it's declared in web.xml)
// "WEB-INF/hapi-fhir-server-database-config.xml", myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext();
// "WEB-INF/hapi-fhir-server-config.xml"
// );
// These two parmeters are also declared in web.xml
String implDesc = getInitParameter("ImplementationDescription");
String fhirVersionParam = getInitParameter("FhirVersion"); String fhirVersionParam = getInitParameter("FhirVersion");
if (StringUtils.isBlank(fhirVersionParam)) { if (StringUtils.isBlank(fhirVersionParam)) {
fhirVersionParam="DSTU1"; fhirVersionParam = "DSTU1";
} }
myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); // Depending on the version this server is supporing, we will
// retrieve all the appropriate resource providers and the
// conformance provider
List<IResourceProvider> beans; List<IResourceProvider> beans;
JpaSystemProvider systemProvider; JpaSystemProvider systemProvider;
IFhirSystemDao systemDao; IFhirSystemDao systemDao;
switch (fhirVersionParam.trim().toUpperCase()) { switch (fhirVersionParam.trim().toUpperCase()) {
case "DSTU": case "DSTU":
case "DSTU1": case "DSTU1": {
setFhirContext(FhirContext.forDstu1()); setFhirContext(FhirContext.forDstu1());
beans = myAppCtx.getBean("myResourceProvidersDstu1", List.class); beans = myAppCtx.getBean("myResourceProvidersDstu1", List.class);
systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProvider.class); systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProvider.class);
systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class);
JpaConformanceProviderDstu1 confProvider = new JpaConformanceProviderDstu1(this, systemDao);
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
break; break;
case "DEV": }
case "DEV": {
setFhirContext(FhirContext.forDev()); setFhirContext(FhirContext.forDev());
beans = myAppCtx.getBean("myResourceProvidersDev", List.class); beans = myAppCtx.getBean("myResourceProvidersDev", List.class);
systemProvider = myAppCtx.getBean("mySystemProviderDev", JpaSystemProvider.class); systemProvider = myAppCtx.getBean("mySystemProviderDev", JpaSystemProvider.class);
systemDao = myAppCtx.getBean("mySystemDaoDev", IFhirSystemDao.class); systemDao = myAppCtx.getBean("mySystemDaoDev", IFhirSystemDao.class);
JpaConformanceProviderDev confProvider = new JpaConformanceProviderDev(this, systemDao);
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
break; break;
}
default: default:
throw new ServletException("Unknown FHIR version specified in init-param[FhirVersion]: " + fhirVersionParam); throw new ServletException("Unknown FHIR version specified in init-param[FhirVersion]: " + fhirVersionParam);
} }
FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
for (IResourceProvider nextResourceProvider : beans) { for (IResourceProvider nextResourceProvider : beans) {
ourLog.info(" * Have resource provider for: {}", nextResourceProvider.getResourceType().getSimpleName()); ourLog.info(" * Have resource provider for: {}", nextResourceProvider.getResourceType().getSimpleName());
} }
setResourceProviders(beans); setResourceProviders(beans);
setPlainProviders(systemProvider); setPlainProviders(systemProvider);
String implDesc = getInitParameter("ImplementationDescription"); FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
JpaConformanceProvider confProvider = new JpaConformanceProvider(this, systemDao);
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
setUseBrowserFriendlyContentTypes(true); setUseBrowserFriendlyContentTypes(true);
String baseUrl = System.getProperty("fhir.baseurl"); String baseUrl = System.getProperty("fhir.baseurl");
if (StringUtils.isBlank(baseUrl)) { if (StringUtils.isBlank(baseUrl)) {
throw new ServletException("Missing system property: fhir.baseurl"); throw new ServletException("Missing system property: fhir.baseurl");
} }
setServerAddressStrategy(new HardcodedServerAddressStrategy(baseUrl)); setServerAddressStrategy(new HardcodedServerAddressStrategy(baseUrl));
setPagingProvider(new FifoMemoryPagingProvider(10)); setPagingProvider(new FifoMemoryPagingProvider(10));
LoggingInterceptor loggingInterceptor = new LoggingInterceptor(); LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
loggingInterceptor.setLoggerName("fhirtest.access"); loggingInterceptor.setLoggerName("fhirtest.access");
loggingInterceptor.setMessageFormat("Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}]"); loggingInterceptor.setMessageFormat("Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}]");
this.registerInterceptor(loggingInterceptor); this.registerInterceptor(loggingInterceptor);
} }
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
// myAppCtx.close(); // myAppCtx.close();
// //
// try { // try {
// ourLog.info("Shutting down derby"); // ourLog.info("Shutting down derby");
// DriverManager.getConnection("jdbc:derby:directory:" + System.getProperty("fhir.db.location") + ";shutdown=true"); // DriverManager.getConnection("jdbc:derby:directory:" + System.getProperty("fhir.db.location") + ";shutdown=true");
// } catch (Exception e) { // } catch (Exception e) {
// ourLog.info("Failed to create database: {}",e.getMessage()); // ourLog.info("Failed to create database: {}",e.getMessage());
// } // }
} }
} }

View File

@ -28,8 +28,10 @@
--> -->
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyTenSevenDialect" /> <property name="hibernate.dialect" value="org.hibernate.dialect.DerbyTenSevenDialect" />
<property name="hibernate.hbm2ddl.auto" value="update" /> <property name="hibernate.hbm2ddl.auto" value="update" />
<!--
<property name="hibernate.connection.username" value="sa" /> <property name="hibernate.connection.username" value="sa" />
<property name="hibernate.connection.password" value="" /> <property name="hibernate.connection.password" value="" />
-->
<property name="hibernate.jdbc.batch_size" value="0" /> <property name="hibernate.jdbc.batch_size" value="0" />
<property name="hibernate.cache.use_minimal_puts" value="false" /> <property name="hibernate.cache.use_minimal_puts" value="false" />
<property name="hibernate.show_sql" value="false" /> <property name="hibernate.show_sql" value="false" />

View File

@ -25,7 +25,7 @@
<bean id="dbServer" class="ca.uhn.fhirtest.DerbyNetworkServer"> <bean id="dbServer" class="ca.uhn.fhirtest.DerbyNetworkServer">
</bean> </bean>
<bean depends-on="dbServer" id="myPersistenceDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <bean depends-on="dbServer" id="myPersistenceDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<!-- ;create=true /opt/glassfish/glassfish4/glassfish/nodes/localhost-domain1/fhirtest/fhirdb --> <!-- ;create=true /opt/glassfish/glassfish4/glassfish/nodes/localhost-domain1/fhirtest/fhirdb -->
<!-- <property name="url" value="jdbc:hsqldb:hsql://localhost/uhnfhirdb"/>--> <!-- <property name="url" value="jdbc:hsqldb:hsql://localhost/uhnfhirdb"/>-->
<!-- <property name="url" value="jdbc:derby:directory:#{systemproperties['fhir.db.location']};create=true" /> --> <!-- <property name="url" value="jdbc:derby:directory:#{systemproperties['fhir.db.location']};create=true" /> -->

View File

@ -13,9 +13,9 @@
<property name="servers"> <property name="servers">
<list> <list>
<value>home , DSTU1 , UHN/HAPI Server (DSTU1 FHIR) , http://fhirtest.uhn.ca/baseDstu1</value> <value>home , DSTU1 , UHN/HAPI Server (DSTU1 FHIR) , http://fhirtest.uhn.ca/baseDstu1</value>
<value>home_dev , DEV , UHN/HAPI Server (DSTU2 FHIR) , http://fhirtest.uhn.ca/baseDev</value> <value>home_dev , DEV , UHN/HAPI Server (DSTU2 FHIR) , http://fhirtest.uhn.ca/baseDev</value>
<value>hi , DSTU1 , Health Intersections (DSTU1 FHIR) , http://fhir.healthintersections.com.au/open</value> <value>hi , DSTU1 , Health Intersections (DSTU1 FHIR) , http://fhir.healthintersections.com.au/open</value>
<value>hidev , DEV , Health Intersections (DSTU2 FHIR) , http://fhir-dev.healthintersections.com.au/open</value> <value>hidev , DEV , Health Intersections (DSTU2 FHIR) , http://fhir-dev.healthintersections.com.au/open</value>
<value>furore , DSTU1 , Spark - Furore , http://spark.furore.com/fhir</value> <value>furore , DSTU1 , Spark - Furore , http://spark.furore.com/fhir</value>
<value>blaze , DSTU1 , Blaze (Orion Health) , https://fhir.orionhealth.com/blaze/fhir</value> <value>blaze , DSTU1 , Blaze (Orion Health) , https://fhir.orionhealth.com/blaze/fhir</value>
<value>oridashi , DSTU1 , Oridashi , http://demo.oridashi.com.au:8190</value> <value>oridashi , DSTU1 , Oridashi , http://demo.oridashi.com.au:8190</value>

View File

@ -2,3 +2,4 @@
/.settings/ /.settings/
.classpath .classpath
.project .project
/target/

View File

@ -41,7 +41,7 @@ public class FhirDev implements IFhirVersion {
private String myId; private String myId;
@Override @Override
public Object createServerConformanceProvider(RestfulServer theServer) { public ServerConformanceProvider createServerConformanceProvider(RestfulServer theServer) {
return new ServerConformanceProvider(theServer); return new ServerConformanceProvider(theServer);
} }

View File

@ -28,6 +28,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
@ -58,6 +60,7 @@ import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter; import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -71,7 +74,7 @@ import ca.uhn.fhir.util.ExtensionConstants;
* <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor. * <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor.
* </p> * </p>
*/ */
public class ServerConformanceProvider { public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true; private boolean myCache = true;
private volatile Conformance myConformance; private volatile Conformance myConformance;
@ -90,13 +93,9 @@ public class ServerConformanceProvider {
return myPublisher; return myPublisher;
} }
/** @Override
* Actually create and return the conformance statement
*
* See the class documentation for an important note if you are extending this class
*/
@Metadata @Metadata
public Conformance getServerConformance() { public Conformance getServerConformance(HttpServletRequest theRequest) {
if (myConformance != null && myCache) { if (myConformance != null && myCache) {
return myConformance; return myConformance;
} }
@ -136,7 +135,7 @@ public class ServerConformanceProvider {
String resourceName = next.getResourceName(); String resourceName = next.getResourceName();
RuntimeResourceDefinition def = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); RuntimeResourceDefinition def = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
resource.getTypeElement().setValue(def.getName()); resource.getTypeElement().setValue(def.getName());
resource.getProfile().setReference(new IdDt(def.getResourceProfile())); resource.getProfile().setReference(new IdDt(def.getResourceProfile(myRestfulServer.getServerBaseForRequest(theRequest))));
TreeSet<String> includes = new TreeSet<String>(); TreeSet<String> includes = new TreeSet<String>();

View File

@ -1,6 +1,10 @@
package ca.uhn.fhir.parser; package ca.uhn.fhir.parser;
import static org.junit.Assert.*; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import net.sf.json.JSON; import net.sf.json.JSON;
import net.sf.json.JSONSerializer; import net.sf.json.JSONSerializer;
import net.sf.json.JsonConfig; import net.sf.json.JsonConfig;
@ -11,16 +15,17 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dev.resource.MedicationPrescription; import ca.uhn.fhir.model.dev.resource.MedicationPrescription;
import ca.uhn.fhir.model.dev.resource.Patient; import ca.uhn.fhir.model.dev.resource.Patient;
import ca.uhn.fhir.model.dev.resource.QuestionnaireAnswers;
import ca.uhn.fhir.model.dstu.resource.Binary; import ca.uhn.fhir.model.dstu.resource.Binary;
import ca.uhn.fhir.model.primitive.BooleanDt; import ca.uhn.fhir.model.primitive.BooleanDt;
import ca.uhn.fhir.model.primitive.CodeDt; import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
public class JsonParserTest { public class JsonParserTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserTest.class);
@ -37,6 +42,23 @@ public class JsonParserTest {
assertEquals("{\"resourceType\":\"Binary\",\"id\":\"11\",\"meta\":{\"versionId\":\"22\"},\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", val); assertEquals("{\"resourceType\":\"Binary\",\"id\":\"11\",\"meta\":{\"versionId\":\"22\"},\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", val);
} }
/**
* #65
*/
@Test
public void testJsonPrimitiveWithExtensionEncoding() {
QuestionnaireAnswers parsed = new QuestionnaireAnswers();
parsed.getGroup().setLinkId("value123");
parsed.getGroup().getLinkIdElement().addUndeclaredExtension(false, "http://123", new StringDt("HELLO"));
String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(parsed);
ourLog.info(encoded);
assertThat(encoded, containsString("{\"linkId\":\"value123\",\"_linkId\":{\"http://123\":[{\"valueString\":\"HELLO\"}]}}"));
}
@Test @Test
public void testParsePatientInBundle() { public void testParsePatientInBundle() {

View File

@ -4,11 +4,16 @@ import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -46,7 +51,7 @@ public class ServerConformanceProviderTest {
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
rs.init(null); rs.init(createServletConfig());
boolean found=false; boolean found=false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
@ -60,7 +65,7 @@ public class ServerConformanceProviderTest {
} }
} }
assertTrue(found); assertTrue(found);
Conformance conformance = sc.getServerConformance(); Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf); ourLog.info(conf);
@ -80,11 +85,11 @@ public class ServerConformanceProviderTest {
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
rs.init(null); rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(); Conformance conformance = sc.getServerConformance(createHttpServletRequest());
myCtx.newValidator().validate(conformance); assertTrue(myCtx.newValidator().validateWithResult(conformance).isSuccessful());
} }
@ -98,7 +103,7 @@ public class ServerConformanceProviderTest {
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
rs.init(null); rs.init(createServletConfig());
boolean found=false; boolean found=false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
@ -113,7 +118,7 @@ public class ServerConformanceProviderTest {
} }
assertTrue(found); assertTrue(found);
Conformance conformance = sc.getServerConformance(); Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = new FhirContext().newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); String conf = new FhirContext().newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf); ourLog.info(conf);
@ -131,9 +136,9 @@ public class ServerConformanceProviderTest {
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
rs.init(null); rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(); Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = new FhirContext().newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); String conf = new FhirContext().newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf); ourLog.info(conf);
@ -171,7 +176,6 @@ public class ServerConformanceProviderTest {
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
@SuppressWarnings("unused")
public static class MultiOptionalProvider { public static class MultiOptionalProvider {
@Search(type = Patient.class) @Search(type = Patient.class)
@ -203,4 +207,19 @@ public class ServerConformanceProviderTest {
} }
private HttpServletRequest createHttpServletRequest() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search");
when(req.getServletPath()).thenReturn("/fhir");
when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search"));
when(req.getContextPath()).thenReturn("/FhirStorm");
return req;
}
private ServletConfig createServletConfig() {
ServletConfig sc = mock(ServletConfig.class);
when (sc.getServletContext()).thenReturn(null);
return sc;
}
} }

View File

@ -6,3 +6,4 @@
.classpath .classpath
.project .project
/target/ /target/
/target/

View File

@ -70,6 +70,7 @@ import ca.uhn.fhir.model.dstu.valueset.DataTypeEnum;
import ca.uhn.fhir.model.dstu.valueset.SlicingRulesEnum; import ca.uhn.fhir.model.dstu.valueset.SlicingRulesEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.provider.ServerConformanceProvider; import ca.uhn.fhir.rest.server.provider.ServerConformanceProvider;
import ca.uhn.fhir.rest.server.provider.ServerProfileProvider; import ca.uhn.fhir.rest.server.provider.ServerProfileProvider;
@ -81,7 +82,7 @@ public class FhirDstu1 implements IFhirVersion {
private String myId; private String myId;
@Override @Override
public Object createServerConformanceProvider(RestfulServer theServer) { public ServerConformanceProvider createServerConformanceProvider(RestfulServer theServer) {
return new ServerConformanceProvider(theServer); return new ServerConformanceProvider(theServer);
} }

View File

@ -55,6 +55,7 @@ import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter; import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.ExtensionConstants;
@ -71,7 +72,7 @@ import javax.servlet.http.HttpServletRequest;
* <code>setCache(false)</code> in your provider constructor. * <code>setCache(false)</code> in your provider constructor.
* </p> * </p>
*/ */
public class ServerConformanceProvider { public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true; private boolean myCache = true;
private volatile Conformance myConformance; private volatile Conformance myConformance;
@ -106,7 +107,7 @@ public class ServerConformanceProvider {
retVal.setPublisher(myPublisher); retVal.setPublisher(myPublisher);
retVal.setDate(DateTimeDt.withCurrentTime()); retVal.setDate(DateTimeDt.withCurrentTime());
retVal.setFhirVersion("0.80"); // TODO: pull from model retVal.setFhirVersion("0.0.82-3059"); // TODO: pull from model
retVal.setAcceptUnknown(false); // TODO: make this configurable - this is a fairly big effort since the parser needs to be modified to actually allow it retVal.setAcceptUnknown(false); // TODO: make this configurable - this is a fairly big effort since the parser needs to be modified to actually allow it
retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription()); retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription());

View File

@ -49,6 +49,7 @@ import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.resource.Profile; import ca.uhn.fhir.model.dstu.resource.Profile;
import ca.uhn.fhir.model.dstu.resource.Query; import ca.uhn.fhir.model.dstu.resource.Query;
import ca.uhn.fhir.model.dstu.resource.Questionnaire;
import ca.uhn.fhir.model.dstu.resource.Specimen; import ca.uhn.fhir.model.dstu.resource.Specimen;
import ca.uhn.fhir.model.dstu.resource.ValueSet; import ca.uhn.fhir.model.dstu.resource.ValueSet;
import ca.uhn.fhir.model.dstu.resource.ValueSet.Define; import ca.uhn.fhir.model.dstu.resource.ValueSet.Define;
@ -89,6 +90,23 @@ public class JsonParserTest {
} }
/**
* #65
*/
@Test
public void testJsonPrimitiveWithExtensionEncoding() {
String res = "{\"resourceType\":\"Questionnaire\",\"status\":\"draft\",\"authored\":\"2014-10-30T14:15:00\",\"subject\":{\"reference\":\"http://www.hl7.org/fhir/Patient/1\"},\"author\":{\"reference\":\"http://www.hl7.org/fhir/Practitioner/1\"},\"name\":{\"text\":\"WDHB Friends and Family Test\"},\"group\":{\"header\":\"Note: This is an anonymous survey, which means you cannot be identified.\",\"_header\":[{\"extension\":[{\"url\":\"http://hl7.org/fhir/Profile/iso-21090#language\",\"valueCode\":\"en\"},{\"url\":\"http://hl7.org/fhir/Profile/iso-21090#string-translation\",\"valueString\":\"è«\\u008b注æ\\u0084\\u008fï¼\\u009aè¿\\u0099æ\\u0098¯ä¸\\u0080个å\\u008c¿å\\u0090\\u008dè°\\u0083æ\\u009f¥ï¼\\u008c被è°\\u0083æ\\u009f¥äººå°\\u0086ä¸\\u008dä¼\\u009a被è¯\\u0086å\\u0088«å\\u0087ºæ\\u009d¥ã\\u0080\\u0082\"},{\"url\":\"http://hl7.org/fhir/Profile/iso-21090#string-translation\",\"valueString\":\"ì\\u009dµëª\\u0085ì\\u009c¼ë¡\\u009c í\\u0095\\u0098ë\\u008a\\u0094 ì\\u0084¤ë¬¸ì¡°ì\\u0082¬ì\\u009d´ë¯\\u0080ë¡\\u009c ì\\u009e\\u0091ì\\u0084±ì\\u009e\\u0090ê°\\u0080 ë\\u0088\\u0084구ì\\u009d¸ì§\\u0080 ë°\\u009dí\\u0098\\u0080ì§\\u0080ì§\\u0080 ì\\u0095\\u008aì\\u008aµë\\u008b\\u0088ë\\u008b¤.\"}]}],\"question\":[{\"extension\":[{\"url\":\"http://hl7.org/fhir/questionnaire-extensions#answerFormat\",\"valueCode\":\"single-choice\"}],\"text\":\"Are you a patient?\",\"options\":{\"reference\":\"#question1\"}}]}}";
Questionnaire parsed = ourCtx.newJsonParser().parseResource(Questionnaire.class, res);
assertEquals("Note: This is an anonymous survey, which means you cannot be identified.", parsed.getGroup().getHeader().getValue());
assertEquals(1, parsed.getGroup().getHeader().getUndeclaredExtensionsByUrl("http://hl7.org/fhir/Profile/iso-21090#language").size());
String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(parsed);
ourLog.info(encoded);
assertThat(encoded, containsString("\"_header\":{"));
}
@Test @Test
public void testEncodeNonContained() { public void testEncodeNonContained() {
Organization org = new Organization(); Organization org = new Organization();
@ -193,7 +211,7 @@ public class JsonParserTest {
encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(c); encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(c);
ourLog.info(encoded); ourLog.info(encoded);
assertEquals(encoded, "{\"resourceType\":\"Conformance\",\"_acceptUnknown\":[{\"extension\":[{\"url\":\"http://foo\",\"valueString\":\"AAA\"}]}]}"); assertEquals(encoded, "{\"resourceType\":\"Conformance\",\"_acceptUnknown\":{\"extension\":[{\"url\":\"http://foo\",\"valueString\":\"AAA\"}]}}");
// Now with a value // Now with a value
ourLog.info("---------------"); ourLog.info("---------------");
@ -207,7 +225,7 @@ public class JsonParserTest {
encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(c); encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(c);
ourLog.info(encoded); ourLog.info(encoded);
assertEquals(encoded, "{\"resourceType\":\"Conformance\",\"acceptUnknown\":true,\"_acceptUnknown\":[{\"extension\":[{\"url\":\"http://foo\",\"valueString\":\"AAA\"}]}]}"); assertEquals(encoded, "{\"resourceType\":\"Conformance\",\"acceptUnknown\":true,\"_acceptUnknown\":{\"extension\":[{\"url\":\"http://foo\",\"valueString\":\"AAA\"}]}}");
} }

View File

@ -13,7 +13,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.provider.JpaConformanceProvider; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu1;
import ca.uhn.fhir.jpa.provider.JpaSystemProvider; import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
import ca.uhn.fhir.jpa.rp.dev.DiagnosticReportResourceProvider; import ca.uhn.fhir.jpa.rp.dev.DiagnosticReportResourceProvider;
import ca.uhn.fhir.jpa.rp.dev.ObservationResourceProvider; import ca.uhn.fhir.jpa.rp.dev.ObservationResourceProvider;

View File

@ -43,7 +43,7 @@ public class FhirDev implements IFhirVersion {
private String myId; private String myId;
@Override @Override
public Object createServerConformanceProvider(RestfulServer theServer) { public ServerConformanceProvider createServerConformanceProvider(RestfulServer theServer) {
return new ServerConformanceProvider(theServer); return new ServerConformanceProvider(theServer);
} }

View File

@ -82,7 +82,7 @@ public class FhirDstu1 implements IFhirVersion {
private String myId; private String myId;
@Override @Override
public Object createServerConformanceProvider(RestfulServer theServer) { public ServerConformanceProvider createServerConformanceProvider(RestfulServer theServer) {
return new ServerConformanceProvider(theServer); return new ServerConformanceProvider(theServer);
} }

View File

@ -55,6 +55,7 @@ import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter; import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.ExtensionConstants;
@ -71,7 +72,7 @@ import javax.servlet.http.HttpServletRequest;
* <code>setCache(false)</code> in your provider constructor. * <code>setCache(false)</code> in your provider constructor.
* </p> * </p>
*/ */
public class ServerConformanceProvider { public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true; private boolean myCache = true;
private volatile Conformance myConformance; private volatile Conformance myConformance;

View File

@ -28,6 +28,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
@ -54,6 +56,7 @@ import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter; import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.ExtensionConstants;
@ -68,7 +71,7 @@ import ca.uhn.fhir.util.ExtensionConstants;
* <code>setCache(false)</code> in your provider constructor. * <code>setCache(false)</code> in your provider constructor.
* </p> * </p>
*/ */
public class ServerConformanceProvider { public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true; private boolean myCache = true;
private volatile Conformance myConformance; private volatile Conformance myConformance;
@ -94,7 +97,7 @@ public class ServerConformanceProvider {
* See the class documentation for an important note if you are extending this class * See the class documentation for an important note if you are extending this class
*/ */
@Metadata @Metadata
public Conformance getServerConformance() { public Conformance getServerConformance(HttpServletRequest theReq) {
if (myConformance != null && myCache) { if (myConformance != null && myCache) {
return myConformance; return myConformance;
} }

View File

@ -11,6 +11,13 @@
Support for DSTU2 features introduced: New Bundle encoding style, as well as new Support for DSTU2 features introduced: New Bundle encoding style, as well as new
extension encoding in JSON. extension encoding in JSON.
</action> </action>
<action type="fix" issue="65">
Fix an issue encoding extensions on primitive types in JSON. Previously the "_value" object
would be an array even if the field it was extending was not repeatable. This is not correct
according to the specification, nor can HAPI's parser parse this correctly. The encoder
has been corrected, and the parser has been adjusted to be able to handle resources with
extensions encoded in this way. Thanks to Mohammad Jafari for reporting!
</action>
<action type="add"> <action type="add">
Library now checks if custom resource types can be instantiated on startup Library now checks if custom resource types can be instantiated on startup
(e.g. because they don't have a no-argument constructor) in order to (e.g. because they don't have a no-argument constructor) in order to

View File

@ -0,0 +1,656 @@
<?xml version="1.0"?>
<document xmlns="http://maven.apache.org/changes/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 ./changes.xsd">
<properties>
<author>James Agnew</author>
<title>HAPI FHIR Changelog</title>
</properties>
<body>
<release version="0.9" date="TBA">
<action type="add">
Support for DSTU2 features introduced: New Bundle encoding style, as well as new
extension encoding in JSON.
</action>
<action type="add">
Library now checks if custom resource types can be instantiated on startup
(e.g. because they don't have a no-argument constructor) in order to
avoid failing later
</action>
<<<<<<< HEAD
<action type="add">
Bump a few dependency JARs to the latest versions in Maven POM:
<![CDATA[
<ul>
<li>SLF4j (in base module) - Bumped to 1.7.9</li>
<li>Apache HTTPClient (in base module) - Bumped to 4.3.6</li>
<li>Hibernate (in JPA module) - Bumped to 4.3.7</li>
</ul>
]]>
=======
<action type="fix" issue="67">
IdDt failed to recognize local identifiers containing fragments that look like
real identifiers as being local identifiers even though they started with '#'.
For example, a local resource reference of "#aa/_history/aa" would be incorrectly
parsed as a non-local reference.
Thanks to Mohammad Jafari for reporting!
>>>>>>> 31d61100db41590b9760daf9e0942ad553ff69e2
</action>
</release>
<release version="0.8" date="2014-Dec-17">
<action type="add">
<![CDATA[<b>API CHANGE:</b>]]> The "FHIR structures" for DSTU1 (the classes which model the
resources and composite datatypes) have been moved out of the core JAR into their
own JAR, in order to allow support for DEV resources, and DSTU2 resources when thast
version is finalized. See
<![CDATA[<a href="./doc_upgrading.html">upgrading</a>]]>
for more information.
</action>
<action type="fix">
<![CDATA[
<b>Deprocated API Removal</b>: The following classes (which were deprocated previously)
have now been removed:
<ul>
<li><b>ISecurityManager</b>: If you are using this class, the same functionality
is available through the more general purpose
<a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_server_interceptor.html">server interceptor</a>
capabilities.
<li><b>CodingListParam</b>: This class was made redundant by the
<a href="http://jamesagnew.github.io/hapi-fhir/apidocs/ca/uhn/fhir/rest/param/TokenOrListParam.html">TokenOrListParam</a>
class, which can be used in its place.
</ul>
]]>
</action>
<action type="add">
<![CDATA[
<b>API Change</b>: The IResource#getResourceMetadata() method has been changed
from returning
<code>Map&lt;ResourceMetadataKeyEnum&lt;?&gt;, Object&gt;<code>
to returning a new type called
<code>ResourceMetadataMap</code>. This new type implements
<code>Map&lt;ResourceMetadataKeyEnum&lt;?&gt;, Object&gt;<code>
itself, so this change should not break existing code, but may
require a clean build in order to run correctly.
]]>
</action>
<action type="add" issue="38" dev="wdebeau1">
Profile generation on the server was not working due to IdDt being
incorrectly used. Thanks to Bill de Beaubien for the pull request!
</action>
<action type="add" issue="42" dev="wdebeau1">
Profiles did not generate correctly if a resource definition class had a
defined extension which was of a composite type. Thanks to Bill de Beaubien for the pull request!
</action>
<action type="add" issue="44" dev="petromykhailysyn">
Remove unnecessary IOException from narrative generator API. Thanks to
Petro Mykhailysyn for the pull request!
</action>
<action type="add" issue="48" dev="wdebeau1">
Introduced a new
<![CDATA[<code>@ProvidesResources</code>]]> annotation which can be added to
resource provider and servers to allow them to declare additional resource
classes they are able to serve. This is useful if you have a server which can
serve up multiple classes for the same resource type (e.g. a server that sometimes
returns a default Patient, but sometimes uses a custom subclass).
Thanks to Bill de Beaubien for the pull request!
</action>
<action type="add" issue="49" dev="wdebeau1">
Introduced a new
<![CDATA[<code>@Destroy</code>]]> annotation which can be added to
a resource provider method. This method will be called by the server when it
is being closed/destroyed (e.g. when the application is being undeployed, the
container is being shut down, etc.)
Thanks to Bill de Beaubien for the pull request!
</action>
<action type="add">
Add a new method <![CDATA[handleException]]> to the server interceptor
framework which allows interceptors to be notified of any exceptions and
runtime errors within server methods. Interceptors may optionally also
override the default error handling behaviour of the RestfulServer.
</action>
<action dev="wdebeau1" type="add">
Add constants to BaseResource for the "_id" search parameter which all resources
should support.
</action>
<action type="fix">
DateRangeParam parameters on the server now return correct
<![CDATA[<code>getLowerBoundAsInstant()</code>]]>
and
<![CDATA[<code>getUpperBoundAsInstant()</code>]]>
values if a single unqualified value is passed in. For example, if
a query containing
<![CDATA[<code>&birthdate=2012-10-01</code>]]>
is received, previously these two methods would both return the same
value, but with this fix
<![CDATA[<code>getUpperBoundAsInstant()</code>]]>
now returns the instant at 23:59:59.9999.
</action>
<action type="fix">
Resource fields with a type of "*" (or Any) sometimes failed to parse if a
value type of "code" was used. Thanks to Bill de Beaubien for reporting!
</action>
<action type="add" dev="lmds">
Remove dependency on JAXB libraries, which were used to parse and encode
dates and times (even in the JSON parser). JAXB is built in to most JDKs
but the version bundled with IBM's JDK is flaky and resulted in a number
of problems when deploying to Websphere.
</action>
<action type="fix" issue="50" dev="jjathman">
Primitive datatypes now preserve their original string value when parsing resources,
as well as containing the "parsed value". For instance, a DecimalDt field value of
<![CDATA[<code>1.0000</code>]]> will be parsed into the corresponding
decimal value, but will also retain the original value with the corresponding
level of precision. This allows vadliator rules to be applied to
original values as received "over the wire", such as well formatted but
invalid dates, e.g. "2001-15-01". Thanks to Joe Athman for reporting and
helping to come up with a fix!
</action>
<action type="add">
When using Generic Client, if performing a
<![CDATA[create]]> or <![CDATA[update]]> operation using a String as the resource body,
the client will auto-detect the FHIR encoding style and send an appropriate
<![CDATA[Content-Type]]> header.
</action>
<action type="fix" issue="52">
JPA module (and public HAPI-FHIR test server) were unable to process resource types
where at least one search parameter has no path specified. These now correctly save
(although the server does not yet process these params, and it should). Thanks to
GitHub user shvoidlee for reporting and help with analysis!
</action>
<action type="fix">
Generic/Fluent Client "create" and "update" method requests were not setting a content type header
</action>
<action type="add" issue="53" dev="petromykhailysyn">
DateDt left precision value as <![CDATA[null]]> in the constructor
<![CDATA[DateDt(Date)]]>.
</action>
<action type="fix">
RESTful server now doesn't overwrite resource IDs if they are absolute. In other words, if
a server's Resource Provider returns a resource with ID "Patient/123" it will be translated to
"[base url]/Patient/123" but if the RP returns ID "http://foo/Patient/123" the ID will be
returned exactly as is. Thanks to Bill de Beaubien for the suggestion!
</action>
<action type="fix" issue="55">
JPA module Transaction operation was not correctly replacing logical IDs
beginning with "cid:" with server assigned IDs, as required by the
specification.
</action>
<action type="fix" dev="tahurac">
<![CDATA[FhirTerser]]> did not visit or find children in contained resources when
searching a resource. This caused server implementations to not always return contained
resources when they are included with a resource being returned.
</action>
<action type="add" dev="lmds">
Add a method <![CDATA[String IResource#getResourceName()]]> which returns the name of the
resource in question (e.g. "Patient", or "Observation"). This is intended as a
convenience to users.
</action>
<action type="fix">
Do not strip version from resource references in resources returned
from server search methods. Thanks to Bill de Beaubien for reporting!
</action>
<action type="fix" dev="jjathman" issue="54">
Correct an issue with the validator where changes to the underlying
OperationOutcome produced by a validation cycle cause the validation
results to be incorrect.
</action>
<action type="fix">
Client interceptors registered to an interface based client instance
were applied to other client instances for the same client interface as well. (Issue
did not affect generic/fluent clients)
</action>
<action type="fix" issue="57">
DateDt, DateTimeDt and types InstantDt types now do not throw an exception
if they are used to parse a value with the wrong level of precision for
the given type but do throw an exception if the wrong level of precision
is passed into their constructors.<![CDATA[<br/><br/>]]>
This means that HAPI FHIR can now successfully parse resources from external
sources that have the wrong level of precision, but will generate a validation
error if the resource is validated. Thanks to Alexander Kley for the suggestion!
</action>
<action type="fix">
Encoding a Binary resource without a content type set should not result in a NullPointerException. Thanks
to Alexander Kley for reporting!
</action>
<action type="add">
Server gives a more helpful error message if multiple IResourceProvider implementations
are provided for the same resource type. Thanks to wanghaisheng for the idea!
</action>
<action type="add" issue="61">
Bring DSTU1 resource definitions up to version 0.0.82-2929<![CDATA[<br/>]]>
Bring DEV resource definitions up to 0.4.0-3775<![CDATA[<br/>]]>
Thanks to crinacimpian for reporting!
</action>
<action type="add" issue="62">
JPA server did not correctly process _include requests if included
resources were present with a non-numeric identifier. Thanks to
Bill de Beaubien for reporting!
</action>
<action type="fix" issue="60">
Client requests which include a resource/bundle body (e.g. create,
update, transaction) were not including a charset in the content type
header, leading to servers incorrectly assuming ISO-8859/1. Thanks to
shvoidlee for reporting!
</action>
<action type="fix" issue="59" dev="wdebeau1">
Clean up the way that Profile resources are automatically exported
by the server for custom resource profile classes. See the
<![CDATA[<a href="http://jamesagnew.github.io/hapi-fhir/apidocs/ca/uhn/fhir/model/api/annotation/ResourceDef.html">@ResourceDef</a>]]>
JavaDoc for information on how this works.
</action>
</release>
<release version="0.7" date="2014-Oct-23">
<action type="add" issue="30">
<![CDATA[<b>API CHANGE:</b>]]> The TagList class previously implemented ArrayList semantics,
but this has been replaced with LinkedHashMap semantics. This means that the list of
tags will no longer accept duplicate tags, but that tag order will still be
preserved. Thanks to Bill de Beaubien for reporting!
</action>
<action type="fix" issue="33">
Server was incorrectly including contained resources being returned as both contained resources, and as
top-level resources in the returned bundle for search operations.
Thanks to Bill de Beaubien for reporting! This also fixes Issue #20, thanks to
lephty for reporting!
</action>
<action type="add" dev="suranga">
Documentation fixes
</action>
<action type="add" dev="dougmartin">
Add a collection of new methods on the generic client which support the
<![CDATA[
<b><a href="./apidocs/ca/uhn/fhir/rest/client/IGenericClient.html#read(java.lang.Class,%20ca.uhn.fhir.model.primitive.UriDt)">read</a></b>,
<b><a href="./apidocs/ca/uhn/fhir/rest/client/IGenericClient.html#vread(java.lang.Class,%20ca.uhn.fhir.model.primitive.UriDt)">read</a></b>,
and <b><a href="./apidocs/ca/uhn/fhir/rest/client/IGenericClient.html#search(java.lang.Class,%20ca.uhn.fhir.model.primitive.UriDt)">search</a></b>
]]>
operations using an absolute URL. This allows developers to perform these operations using
URLs they obtained from other sources (or external resource references within resources). In
addition, the existing read/vread operations will now access absolute URL references if
they are passed in. Thanks to Doug Martin of the Regenstrief Center for Biomedical Informatics
for contributing this implementation!
</action>
<action type="fix">
Server implementation was not correctly figuring out its own FHIR Base URL when deployed
on Amazon Web Service server. Thanks to Jeffrey Ting and Bill De Beaubien of
Systems Made Simple for their help in figuring out this issue!
</action>
<action type="fix">
XML Parser failed to encode fields with both a resource reference child and
a primitive type child. Thanks to Jeffrey Ting and Bill De Beaubien of
Systems Made Simple for their help in figuring out this issue!
</action>
<action type="fix">
HAPI now runs successfully on Servlet 2.5 containers (such as Tomcat 6). Thanks to
Bernard Gitaadji for reporting and diagnosing the issue!
</action>
<action type="fix">
Summary (in the bundle entry) is now encoded by the XML and JSON parsers if supplied. Thanks to David Hay of
Orion Health for reporting this!
</action>
<action type="fix" issue="24">
Conformance profiles which are automatically generated by the server were missing a few mandatory elements,
which meant that the profile did not correctly validate. Thanks to Bill de Beaubien of Systems Made Simple
for reporting this!
</action>
<action type="fix">
XHTML (in narratives) containing escapable characters (e.g. &lt; or &quot;) will now always have those characters
escaped properly in encoded messages.
</action>
<action type="fix">
Resources containing entities which are not valid in basic XML (e.g. &amp;sect;) will have those
entities converted to their equivalent unicode characters when resources are encoded, since FHIR does
not allow extended entities in resource instances.
</action>
<action type="add">
Add a new client interceptor which adds HTTP Authorization Bearer Tokens (for use with OAUTH2 servers)
to client requests.
</action>
<action type="fix">
Add phloc-commons dependency explicitly, which resolves an issue building HAPI from source on
some platforms. Thanks to Odysseas Pentakalos for the patch!
</action>
<action type="add">
HAPI now logs a single line indicating the StAX implementation being used upon the
first time an XML parser is created.
</action>
<action type="fix">
Update methods on the server did not return a "content-location" header, but
only a "location" header. Both are required according to the FHIR specification.
Thanks to Bill de Beaubien of Systems Made Simple for reporting this!
</action>
<action type="fix" issue="26" dev="akley">
Parser failed to correctly read contained Binary resources. Thanks to Alexander Kley for
the patch!
</action>
<action type="fix" issue="29" dev="akley">
Calling encode multiple times on a resource with contained resources caused the contained
resources to be re-added (and the actual message to grow) with each encode pass. Thanks to
Alexander Kley for the test case!
</action>
<action type="fix">
JSON-encoded contained resources with the incorrect "_id" element (which should be "id", but some
incorrect examples exist on the FHIR specification) now parse correctly. In other words, HAPI
previously only accepted the correct "id" element, but now it also accepts the incorrect
"_id" element just to be more lenient.
</action>
<action type="fix">
Several unit tests failed on Windows (or any platform with non UTF-8 default encoding). This may
have also caused resource validation to fail occasionally on these platforms as well.
Thanks to Bill de Beaubien for reporting!
</action>
<action type="fix">
toString() method on TokenParam was incorrectly showing the system as the value.
Thanks to Bill de Beaubien for reporting!
</action>
<action type="update">
Documentation on contained resources contained a typo and did not actually produce contained resources. Thanks
to David Hay of Orion Health for reporting!
</action>
<action type="add" issue="31" dev="preston">
Add a
<![CDATA[<a href="https://www.vagrantup.com/">Vagrant</a>]]>
based environment (basically a fully built, self contained development environment) for
trying out the HAPI server modules. Thanks to Preston Lee for the pull request, and for
offering to maintain this!
</action>
<action type="add" issue="32" dev="jjathman">
Change validation API so that it uses a return type instead of exceptions to communicate
validation failures. Thanks to Joe Athman for the pull request!
</action>
<action type="add" issue="35" dev="petromykhailysyn">
Add a client interceptor which adds an HTTP cookie to each client request. Thanks to
Petro Mykhailysyn for the pull request!
</action>
</release>
<release version="0.6" date="2014-Sep-08" description="This release brings a number of new features and bug fixes!">
<!--
<action type="add">
Allow generic client ... OAUTH
</action>
-->
<action type="add">
Add server interceptor framework, and new interceptor for logging incoming
requests.
</action>
<action type="add">
Add server validation framework for validating resources against the FHIR schemas and schematrons
</action>
<action type="fix">
Tester UI created double _format and _pretty param entries in searches. Thanks to Gered King of University
Health Network for reporting!
</action>
<action type="fix" issue="4">
Create method was incorrectly returning an HTTP 204 on sucessful completion, but
should be returning an HTTP 200 per the FHIR specification. Thanks to wanghaisheng
for reporting!
</action>
<action type="fix">
FHIR Tester UI now correctly sends UTF-8 charset in responses so that message payloads containing
non US-ASCII characters will correctly display in the browser
</action>
<action type="fix">
JSON parser was incorrectly encoding extensions on composite elements outside the element itself
(as is done correctly for non-composite elements) instead of inside of them. Thanks to David Hay of
Orion for reporting this!
</action>
<action type="add">
Contained/included resource instances received by a client are now automatically
added to any ResourceReferenceDt instancea in other resources which reference them.
</action>
<action type="add">
Add documentation on how to use eBay CORS Filter to support Cross Origin Resource
Sharing (CORS) to server. CORS support that was built in to the server itself has
been removed, as it did not work correctly (and was reinventing a wheel that others
have done a great job inventing). Thanks to Peter Bernhardt of Relay Health for all the assistance
in testing this!
</action>
<action type="fix">
IResource interface did not expose the getLanguage/setLanguage methods from BaseResource,
so the resource language was difficult to access.
</action>
<action type="fix">
JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use
of single quotes
</action>
<action type="add">
Transaction server method is now allowed to return an OperationOutcome in addition to the
incoming resources. The public test server now does this in order to return status information
about the transaction processing.
</action>
<action type="add">
Update method in the server can now flag (via a field on the MethodOutcome object being returned)
that the result was actually a creation, and Create method can indicate that it was actually an
update. This has no effect other than to switch between the HTTP 200 and HTTP 201 status codes on the
response, but this may be useful in some circumstances.
</action>
<action type="fix" dev="tahurac">
Annotation client search methods with a specific resource type (e.g. List&lt;Patient&gt; search())
won't return any resources that aren't of the correct type that are received in a response
bundle (generally these are referenced resources, so they are populated in the reference fields instead).
Thanks to Tahura Chaudhry of University Health Network for the unit test!
</action>
<action type="add">
Added narrative generator template for OperationOutcome resource
</action>
<action type="fix">
Date/time types did not correctly parse values in the format "yyyymmdd" (although the FHIR-defined format
is "yyyy-mm-dd" anyhow, and this is correctly handled). Thanks to Jeffrey Ting of Systems Made Simple
for reporting!
</action>
<action type="fix">
Server search method for an unnamed query gets called if the client requests a named query
with the same parameter list. Thanks to Neal Acharya of University Health Network for reporting!
</action>
<action type="fix">
Category header (for tags) is correctly read in client for "read" operation
</action>
<action type="add">
Transaction method in server can now have parameter type Bundle instead of
List&lt;IResource&gt;
</action>
<action type="add">
HAPI parsers now use field access to get/set values instead of method accessors and mutators.
This should give a small performance boost.
</action>
<action type="fix">
JSON parser encodes resource references incorrectly, using the name "resource" instead
of the name "reference" for the actual reference. Thanks to
Ricky Nguyen for reporting and tracking down the issue!
</action>
<action type="fix">
Rename NotImpementedException to NotImplementedException (to correct typo)
</action>
<action type="fix">
Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with 4xx/5xx status)
</action>
<action type="fix">
Fix performance issue in date/time datatypes where pattern matchers were not static
</action>
<action type="fix">
Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid, but
previously lead to a very unhelpful error message). Thanks to Tahura Chaudhry of UHN for reporting!
</action>
<action type="fix">
Resource of type "List" failed to parse from a bundle correctly. Thanks to David Hay of Orion Health
for reporting!
</action>
<action type="fix">
QuantityParam correctly encodes approximate (~) prefix to values
</action>
<action type="fix" issue="14">
If a server defines a method with parameter "_id", incoming search requests for that method may
get delegated to the wrong method. Thanks to Neal Acharya for reporting!
</action>
<action type="add">
SecurityEvent.Object structural element has been renamed to
SecurityEvent.ObjectElement to avoid conflicting names with the
java Object class. Thanks to Laurie Macdougall-Sookraj of UHN for
reporting!
</action>
<action type="fix">
Text/narrative blocks that were created with a non-empty
namespace prefix (e.g. &lt;xhtml:div xmlns:xhtml="..."&gt;...&lt;/xhtml:div&gt;)
failed to encode correctly (prefix was missing in encoded resource)
</action>
<action type="fix">
Resource references previously encoded their children (display and reference)
in the wrong order so references with both would fail schema validation.
</action>
<action type="add">
SecurityEvent resource's enums now use friendly enum names instead of the unfriendly
numeric code values. Thanks to Laurie MacDougall-Sookraj of UHN for the
suggestion!
</action>
</release>
<release version="0.5" date="2014-Jul-30">
<action type="add">
HAPI has a number of RESTful method parameter types that have similar but not identical
purposes and confusing names. A cleanup has been undertaken to clean this up.
This means that a number of existing classes
have been deprocated in favour of new naming schemes.
<![CDATA[<br/><br/>]]>
All annotation-based clients and all server search method parameters are now named
(type)Param, for example: StringParam, TokenParam, etc.
<![CDATA[<br/><br/>]]>
All generic/fluent client method parameters are now named
(type)ClientParam, for example: StringClientParam, TokenClientParam, etc.
<![CDATA[<br/><br/>]]>
All renamed classes have been retained and deprocated, so this change should not cause any issues
for existing applications but those applications should be refactored to use the
new parameters when possible.
</action>
<action type="add">
Allow server methods to return wildcard generic types (e.g. List&lt;? extends IResource&gt;)
</action>
<action type="add">
Search parameters are not properly escaped and unescaped. E.g. for a token parameter such as
"&amp;identifier=system|codepart1\|codepart2"
</action>
<action type="add">
Add support for OPTIONS verb (which returns the server conformance statement)
</action>
<action type="add">
Add support for CORS headers in server
</action>
<action type="add">
Bump SLF4j dependency to latest version (1.7.7)
</action>
<action type="add">
Add interceptor framework for clients (annotation based and generic), and add interceptors
for configurable logging, capturing requests and responses, and HTTP basic auth.
</action>
<action type="fix">
Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir" instead
of the correct "application/atom+xml"). Thanks to David Hay of Orion Health for surfacing this one!
</action>
<action type="add">
Bundle entries now support a link type of "search". Thanks to David Hay for the suggestion!
</action>
<action type="add" issue="1">
If a client receives a non 2xx response (e.g. HTTP 500) and the response body is a text/plain message or
an OperationOutcome resource, include the message in the exception message so that it will be
more conveniently displayed in logs and other places. Thanks to Neal Acharya for the suggestion!
</action>
<action type="add" issue="2">
Read invocations in the client now process the "Content-Location" header and use it to
populate the ID of the returned resource. Thanks to Neal Acharya for the suggestion!
</action>
<action type="fix" issue="3">
Fix issue where vread invocations on server incorrectly get routed to instance history method if one is
defined. Thanks to Neal Acharya from UHN for surfacing this one!
</action>
<action type="add">
Binary reads on a server not include the Content-Disposition header, to prevent HTML in binary
blobs from being used for nefarious purposes. See
<![CDATA[<a href="http://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemEdit&tracker_id=677&tracker_item_id=3298">FHIR Tracker Bug 3298</a>]]>
for more information.
</action>
<action type="add">
Support has been added for using an HTTP proxy for outgoing requests.
</action>
<action type="fix">
Fix: Primitive extensions declared against custom resource types
are encoded even if they have no value. Thanks to David Hay of Orion for
reporting this!
</action>
<action type="fix">
Fix: RESTful server deployed to a location where the URL to access it contained a
space (e.g. a WAR file with a space in the name) failed to work correctly.
Thanks to David Hay of Orion for reporting this!
</action>
</release>
<release version="0.4" date="2014-Jul-13">
<action type="add">
<![CDATA[<b>BREAKING CHANGE:</b>]]>: IdDt has been modified so that it
contains a partial or complete resource identity. Previously it contained
only the simple alphanumeric id of the resource (the part at the end of the "read" URL for
that resource) but it can now contain a complete URL or even a partial URL (e.g. "Patient/123")
and can optionally contain a version (e.g. "Patient/123/_history/456"). New methods have
been added to this datatype which provide just the numeric portion. See the JavaDoc
for more information.
</action>
<action type="add">
<![CDATA[<b>API CHANGE:</b>]]>: Most elements in the HAPI FHIR model contain
a getId() and setId() method. This method is confusing because it is only actually used
for IDREF elements (which are rare) but its name makes it easy to confuse with more
important identifiers. For this reason, these methods have been deprocated and replaced with
get/setElementSpecificId() methods. The old methods will be removed at some point. Resource
types are unchanged and retain their get/setId methods.
</action>
<action type="add">
Allow use of QuantityDt as a service parameter to support the "quantity" type. Previously
QuantityDt did not implement IQueryParameterType so it was not valid, and there was no way to
support quantity search parameters on the server (e.g. Observation.value-quantity)
</action>
<action type="add">
Introduce StringParameter type which can be used as a RESTful operation search parameter
type. StringParameter allows ":exact" matches to be specified in clients, and handled in servers.
</action>
<action type="add">
Parsers (XML/JSON) now support deleted entries in bundles
</action>
<action type="add">
Transaction method now supported in servers
</action>
<action type="add">
Support for Binary resources added (in servers, clients, parsers, etc.)
</action>
<action type="fix">
Support for Query resources fixed (in parser)
</action>
<action type="fix">
Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a resource)
now parse and encode correctly, meaning that all contained resources are placed in the "contained" element
of the root resource, and the parser looks in the root resource for all container levels when stitching
contained resources back together.
</action>
<action type="fix">
Server methods with @Include parameter would sometimes fail when no _include was actually
specified in query strings.
</action>
<action type="fix">
Client requests for IdentifierDt types (such as Patient.identifier) did not create the correct
query string if the system is null.
</action>
<action type="add">
Add support for paging responses from RESTful servers.
</action>
<action type="fix">
Don't fail on narrative blocks in JSON resources with only an XML declaration but no content (these are
produced by the Health Intersections server)
</action>
<action type="fix">
Server now automatically compresses responses if the client indicates support
</action>
<action type="fix">
Server failed to support optional parameters when type is String and :exact qualifier is used
</action>
<action type="fix">
Read method in client correctly populated resource ID in returned object
</action>
<action type="add">
Support added for deleted-entry by/name, by/email, and comment from Tombstones spec
</action>
</release>
<release version="0.3" date="2014-May-12" description="This release corrects lots of bugs and introduces the fluent client mode">
</release>
</body>
</document>