diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 712cb060e23..8b0168ae690 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -118,6 +118,7 @@ changelog.txt + javac.bat diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java index 80fcb05295c..a705ab91fd7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java @@ -78,6 +78,10 @@ public enum FhirVersionEnum { return myVersionImplementation; } + public boolean isEqualOrNewerThan(FhirVersionEnum theVersion) { + return ordinal() >= theVersion.ordinal(); + } + public boolean isEquivalentTo(FhirVersionEnum theVersion) { if (this.equals(theVersion)) { return true; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 2e3a3563413..f759cefa6cd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -19,19 +19,6 @@ package ca.uhn.fhir.parser; * limitations under the License. * #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.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.ValueType; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.util.BinaryUtil; 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.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 @@ -89,7 +93,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private boolean addToHeldExtensions(int valueIdx, List> ext, ArrayList> list, boolean theIsModifier, CompositeChildElement theChildElem, - CompositeChildElement theParent) { + CompositeChildElement theParent) { if (ext.size() > 0) { list.ensureCapacity(valueIdx); while (list.size() <= valueIdx) { @@ -140,12 +144,6 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { 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 { if (myPrettyPrint) { theEventWriter.setPrettyPrint(myPrettyPrint); @@ -157,6 +155,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { theEventWriter.flush(); } + @Override + protected void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException { + JsonLikeWriter eventWriter = createJsonWriter(theWriter); + doEncodeResourceToJsonLikeWriter(theResource, eventWriter); + } + @Override public T doParseResource(Class theResourceType, Reader theReader) { JsonLikeStructure jsonStructure = new GsonStructure(); @@ -192,136 +196,136 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBase theNextValue, - BaseRuntimeElementDefinition theChildDef, String theChildName, boolean theContainedResource, boolean theSubResource, CompositeChildElement theChildElem, - boolean theForceEmpty) throws IOException { + BaseRuntimeElementDefinition theChildDef, String theChildName, boolean theContainedResource, boolean theSubResource, CompositeChildElement theChildElem, + boolean theForceEmpty) throws IOException { switch (theChildDef.getChildType()) { - case ID_DATATYPE: { - IIdType value = (IIdType) theNextValue; - String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue(); - if (isBlank(encodedValue)) { - break; - } - if (theChildName != null) { - write(theEventWriter, theChildName, encodedValue); - } else { - theEventWriter.write(encodedValue); - } - break; - } - case PRIMITIVE_DATATYPE: { - final IPrimitiveType value = (IPrimitiveType) theNextValue; - if (isBlank(value.getValueAsString())) { - if (theForceEmpty) { - theEventWriter.writeNull(); + case ID_DATATYPE: { + IIdType value = (IIdType) theNextValue; + String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue(); + if (isBlank(encodedValue)) { + break; + } + if (theChildName != null) { + write(theEventWriter, theChildName, encodedValue); + } else { + theEventWriter.write(encodedValue); } break; } - - if (value instanceof IBaseIntegerDatatype) { - if (theChildName != null) { - write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue()); - } else { - theEventWriter.write(((IBaseIntegerDatatype) value).getValue()); - } - } else if (value instanceof IBaseDecimalDatatype) { - BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue(); - decimalValue = new BigDecimal(decimalValue.toString()) { - private static final long serialVersionUID = 1L; - - @Override - public String toString() { - return value.getValueAsString(); + case PRIMITIVE_DATATYPE: { + final IPrimitiveType value = (IPrimitiveType) theNextValue; + if (isBlank(value.getValueAsString())) { + if (theForceEmpty) { + theEventWriter.writeNull(); } - }; - if (theChildName != null) { - write(theEventWriter, theChildName, decimalValue); - } else { - theEventWriter.write(decimalValue); + break; } - } else if (value instanceof IBaseBooleanDatatype) { - if (theChildName != null) { - write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue()); + + if (value instanceof IBaseIntegerDatatype) { + if (theChildName != null) { + write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue()); + } else { + theEventWriter.write(((IBaseIntegerDatatype) value).getValue()); + } + } else if (value instanceof IBaseDecimalDatatype) { + BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue(); + decimalValue = new BigDecimal(decimalValue.toString()) { + private static final long serialVersionUID = 1L; + + @Override + public String toString() { + return value.getValueAsString(); + } + }; + if (theChildName != null) { + write(theEventWriter, theChildName, decimalValue); + } else { + theEventWriter.write(decimalValue); + } + } else if (value instanceof IBaseBooleanDatatype) { + if (theChildName != null) { + write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue()); + } else { + Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue(); + if (booleanValue != null) { + theEventWriter.write(booleanValue.booleanValue()); + } + } } else { - Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue(); - if (booleanValue != null) { - theEventWriter.write(booleanValue.booleanValue()); + String valueStr = value.getValueAsString(); + if (theChildName != null) { + write(theEventWriter, theChildName, valueStr); + } else { + theEventWriter.write(valueStr); } } - } else { - String valueStr = value.getValueAsString(); + break; + } + case RESOURCE_BLOCK: + case COMPOSITE_DATATYPE: { if (theChildName != null) { - write(theEventWriter, theChildName, valueStr); + theEventWriter.beginObject(theChildName); } else { - theEventWriter.write(valueStr); + theEventWriter.beginObject(); } + encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theChildElem); + theEventWriter.endObject(); + break; } - break; - } - case RESOURCE_BLOCK: - case COMPOSITE_DATATYPE: { - if (theChildName != null) { - theEventWriter.beginObject(theChildName); - } else { - theEventWriter.beginObject(); - } - encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theChildElem); - theEventWriter.endObject(); - break; - } - case CONTAINED_RESOURCE_LIST: - case CONTAINED_RESOURCES: { + case CONTAINED_RESOURCE_LIST: + case CONTAINED_RESOURCES: { /* * Disabled per #103 ContainedDt value = (ContainedDt) theNextValue; for (IResource next : * value.getContainedResources()) { if (getContainedResources().getResourceId(next) != null) { continue; } * encodeResourceToJsonStreamWriter(theResDef, next, theWriter, null, true, * fixContainedResourceId(next.getId().getValue())); } */ - List containedResources = getContainedResources().getContainedResources(); - if (containedResources.size() > 0) { - beginArray(theEventWriter, theChildName); + List containedResources = getContainedResources().getContainedResources(); + if (containedResources.size() > 0) { + beginArray(theEventWriter, theChildName); - for (IBaseResource next : containedResources) { - IIdType resourceId = getContainedResources().getResourceId(next); - encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, false, fixContainedResourceId(resourceId.getValue())); - } + for (IBaseResource next : containedResources) { + IIdType resourceId = getContainedResources().getResourceId(next); + encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, false, fixContainedResourceId(resourceId.getValue())); + } - theEventWriter.endArray(); - } - break; - } - case PRIMITIVE_XHTML_HL7ORG: - case PRIMITIVE_XHTML: { - if (!isSuppressNarratives()) { - IPrimitiveType dt = (IPrimitiveType) theNextValue; - if (theChildName != null) { - write(theEventWriter, theChildName, dt.getValueAsString()); - } else { - theEventWriter.write(dt.getValueAsString()); - } - } else { - if (theChildName != null) { - // do nothing - } else { - theEventWriter.writeNull(); + theEventWriter.endArray(); } + break; } - break; - } - case RESOURCE: - IBaseResource resource = (IBaseResource) theNextValue; - RuntimeResourceDefinition def = myContext.getResourceDefinition(resource); - encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true); - break; - case UNDECL_EXT: - default: - throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name()); + case PRIMITIVE_XHTML_HL7ORG: + case PRIMITIVE_XHTML: { + if (!isSuppressNarratives()) { + IPrimitiveType dt = (IPrimitiveType) theNextValue; + if (theChildName != null) { + write(theEventWriter, theChildName, dt.getValueAsString()); + } else { + theEventWriter.write(dt.getValueAsString()); + } + } else { + if (theChildName != null) { + // do nothing + } else { + theEventWriter.writeNull(); + } + } + break; + } + case RESOURCE: + IBaseResource resource = (IBaseResource) theNextValue; + RuntimeResourceDefinition def = myContext.getResourceDefinition(resource); + encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true); + break; + case UNDECL_EXT: + default: + throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name()); } } private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonLikeWriter theEventWriter, - boolean theContainedResource, boolean theSubResource, CompositeChildElement theParent) throws IOException { + boolean theContainedResource, boolean theSubResource, CompositeChildElement theParent) throws IOException { { String elementId = getCompositeElementId(theElement); @@ -336,7 +340,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { BaseRuntimeChildDefinition nextChild = nextChildElem.getDef(); if (nextChildElem.getDef().getElementName().equals("extension") || nextChildElem.getDef().getElementName().equals("modifierExtension") - || nextChild instanceof RuntimeChildDeclaredExtensionDefinition) { + || nextChild instanceof RuntimeChildDeclaredExtensionDefinition) { if (!haveWrittenExtensions) { extractAndWriteExtensionsAsDirectChild(theElement, theEventWriter, myContext.getElementDefinition(theElement.getClass()), theResDef, theResource, nextChildElem, theParent); haveWrittenExtensions = true; @@ -452,15 +456,15 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { if (nextChild.getMax() > 1 || nextChild.getMax() == Child.MAX_UNLIMITED) { beginArray(theEventWriter, childName); 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) { // suppress narratives from contained resources } 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; } else { - encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource,theSubResource, nextChildElem, force); + encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, theSubResource, nextChildElem, force); } valueIdx++; @@ -542,7 +546,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, boolean theSubResource, - CompositeChildElement theParent) throws IOException, DataFormatException { + CompositeChildElement theParent) throws IOException, DataFormatException { writeCommentsPreAndPost(theNextValue, theEventWriter); encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theSubResource, theParent); @@ -555,14 +559,14 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) { throw new IllegalArgumentException( - "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); + "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); } doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter); } private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, - boolean theContainedResource, boolean theSubResource) throws IOException { + boolean theContainedResource, boolean theSubResource) throws IOException { IIdType resourceId = null; // if (theResource instanceof IResource) { // IResource res = (IResource) theResource; @@ -599,7 +603,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, - boolean theContainedResource, boolean theSubResource, IIdType theResourceId) throws IOException { + boolean theContainedResource, boolean theSubResource, IIdType theResourceId) throws IOException { if (!theContainedResource) { super.containResourcesForEncoding(theResource); } @@ -613,28 +617,28 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } write(theEventWriter, "resourceType", resDef.getName()); - if (theResourceId != null && theResourceId.hasIdPart()) { - write(theEventWriter, "id", theResourceId.getIdPart()); - final List extensions = new ArrayList(0); - final List modifierExtensions = new ArrayList(0); - // Undeclared extensions - extractUndeclaredExtensions(theResourceId, extensions, modifierExtensions, null, null); - boolean haveExtension = false; - if (!extensions.isEmpty()) { - haveExtension = true; - } + if (theResourceId != null && theResourceId.hasIdPart()) { + write(theEventWriter, "id", theResourceId.getIdPart()); + final List extensions = new ArrayList(0); + final List modifierExtensions = new ArrayList(0); + // Undeclared extensions + extractUndeclaredExtensions(theResourceId, extensions, modifierExtensions, null, null); + boolean haveExtension = false; + if (!extensions.isEmpty()) { + haveExtension = true; + } - if (theResourceId.hasFormatComment() || haveExtension) { - beginObject(theEventWriter, "_id"); - if (theResourceId.hasFormatComment()) { - writeCommentsPreAndPost(theResourceId, theEventWriter); - } - if (haveExtension) { - writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions); - } - theEventWriter.endObject(); - } - } + if (theResourceId.hasFormatComment() || haveExtension) { + beginObject(theEventWriter, "_id"); + if (theResourceId.hasFormatComment()) { + writeCommentsPreAndPost(theResourceId, theEventWriter); + } + if (haveExtension) { + writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions); + } + theEventWriter.endObject(); + } + } if (theResource instanceof IResource) { IResource resource = (IResource) theResource; @@ -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(); } @@ -731,12 +708,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { /** * This is useful only for the two cases where extensions are encoded as direct children (e.g. not in some object * called _name): resource extensions, and extension extensions - * + * * @param theChildElem * @param theParent */ private void extractAndWriteExtensionsAsDirectChild(IBase theElement, JsonLikeWriter theEventWriter, BaseRuntimeElementDefinition theElementDef, RuntimeResourceDefinition theResDef, - IBaseResource theResource, CompositeChildElement theChildElem, CompositeChildElement theParent) throws IOException { + IBaseResource theResource, CompositeChildElement theChildElem, CompositeChildElement theParent) throws IOException { List extensions = new ArrayList(0); List modifierExtensions = new ArrayList(0); @@ -753,7 +730,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void extractDeclaredExtensions(IBase theResource, BaseRuntimeElementDefinition resDef, List extensions, List modifierExtensions, - CompositeChildElement theChildElem) { + CompositeChildElement theChildElem) { for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsNonModifier()) { for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) { if (nextValue != null) { @@ -777,7 +754,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void extractUndeclaredExtensions(IBase theElement, List extensions, List modifierExtensions, CompositeChildElement theChildElem, - CompositeChildElement theParent) { + CompositeChildElement theParent) { if (theElement instanceof ISupportsUndeclaredExtensions) { ISupportsUndeclaredExtensions element = (ISupportsUndeclaredExtensions) theElement; List ext = element.getUndeclaredExtensions(); @@ -1045,78 +1022,78 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } } - private void parseExtension(ParserState theState, JsonLikeArray theValues, boolean theIsModifier) { - int allUnderscoreNames = 0; - int handledUnderscoreNames = 0; + private void parseExtension(ParserState theState, JsonLikeArray theValues, boolean theIsModifier) { + int allUnderscoreNames = 0; + int handledUnderscoreNames = 0; + + for (int i = 0; i < theValues.size(); i++) { + JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i)); + JsonLikeValue jsonElement = nextExtObj.get("url"); + String url; + if (null == jsonElement || !(jsonElement.isScalar())) { + String parentElementName; + if (theIsModifier) { + parentElementName = "modifierExtension"; + } else { + parentElementName = "extension"; + } + getErrorHandler().missingRequiredElement(new ParseLocation(parentElementName), "url"); + url = null; + } else { + url = getExtensionUrl(jsonElement.getAsString()); + } + theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl()); + for (String next : nextExtObj.keySet()) { + if ("url".equals(next)) { + continue; + } else if ("extension".equals(next)) { + JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next)); + parseExtension(theState, jsonVal, false); + } else if ("modifierExtension".equals(next)) { + JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next)); + parseExtension(theState, jsonVal, true); + } else if (next.charAt(0) == '_') { + allUnderscoreNames++; + continue; + } else { + JsonLikeValue jsonVal = nextExtObj.get(next); + String alternateName = '_' + next; + JsonLikeValue alternateVal = nextExtObj.get(alternateName); + if (alternateVal != null) { + handledUnderscoreNames++; + } + parseChildren(theState, next, jsonVal, alternateVal, alternateName, false); + } + } - for (int i = 0; i < theValues.size(); i++) { - JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i)); - JsonLikeValue jsonElement = nextExtObj.get("url"); - String url; - if (null == jsonElement || !(jsonElement.isScalar())) { - String parentElementName; - if (theIsModifier) { - parentElementName = "modifierExtension"; - } else { - parentElementName = "extension"; - } - getErrorHandler().missingRequiredElement(new ParseLocation(parentElementName), "url"); - url = null; - } else { - url = getExtensionUrl(jsonElement.getAsString()); - } - theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl()); - for (String next : nextExtObj.keySet()) { - if ("url".equals(next)) { - continue; - } else if ("extension".equals(next)) { - JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next)); - parseExtension(theState, jsonVal, false); - } else if ("modifierExtension".equals(next)) { - JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next)); - parseExtension(theState, jsonVal, true); - } else if (next.charAt(0) == '_') { - allUnderscoreNames++; - continue; - } else { - JsonLikeValue jsonVal = nextExtObj.get(next); - String alternateName = '_' + next; - JsonLikeValue alternateVal = nextExtObj.get(alternateName); - if (alternateVal != null) { - handledUnderscoreNames++; - } - parseChildren(theState, next, jsonVal, alternateVal, alternateName, false); - } - } - /* - * This happens if an element has an extension but no actual value. I.e. + * This happens if an element has an extension but no actual value. I.e. * if a resource has a "_status" element but no corresponding "status" * element. This could be used to handle a null value with an extension * for example. */ - if (allUnderscoreNames > handledUnderscoreNames) { - for (String alternateName : nextExtObj.keySet()) { - if (alternateName.startsWith("_") && alternateName.length() > 1) { - JsonLikeValue nextValue = nextExtObj.get(alternateName); - if (nextValue != null) { - if (nextValue.isObject()) { - String nextName = alternateName.substring(1); - if (nextExtObj.get(nextName) == null) { - theState.enteringNewElement(null, nextName); - parseAlternates(nextValue, theState, alternateName, alternateName); - theState.endingElement(); - } - } else { - getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null); - } - } - } - } - } - theState.endingElement(); - } - } + if (allUnderscoreNames > handledUnderscoreNames) { + for (String alternateName : nextExtObj.keySet()) { + if (alternateName.startsWith("_") && alternateName.length() > 1) { + JsonLikeValue nextValue = nextExtObj.get(alternateName); + if (nextValue != null) { + if (nextValue.isObject()) { + String nextName = alternateName.substring(1); + if (nextExtObj.get(nextName) == null) { + theState.enteringNewElement(null, nextName); + parseAlternates(nextValue, theState, alternateName, alternateName); + theState.endingElement(); + } + } else { + getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null); + } + } + } + } + } + theState.endingElement(); + } + } private void parseFhirComments(JsonLikeValue theObject, ParserState theState) { if (theObject.isArray()) { @@ -1270,7 +1247,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } private void writeExtensionsAsDirectChild(IBaseResource theResource, JsonLikeWriter theEventWriter, RuntimeResourceDefinition resDef, List extensions, - List modifierExtensions) throws IOException { + List modifierExtensions) throws IOException { if (extensions.isEmpty() == false) { beginArray(theEventWriter, "extension"); for (HeldExtension next : extensions) { @@ -1344,6 +1321,28 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { 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 extensions = new ArrayList(0); + final List modifierExtensions = new ArrayList(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 { if (myUndeclaredExtension != null) { writeUndeclaredExtension(theResDef, theResource, theEventWriter, myUndeclaredExtension); @@ -1357,7 +1356,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { /* * This makes sure that even if the extension contains a reference to a contained * resource which has a HAPI-assigned ID we'll still encode that ID. - * + * * See #327 */ List preProcessedValue = preProcessValues(myDef, theResource, Collections.singletonList(myValue), myChildElem); @@ -1383,7 +1382,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } else { String childName = myDef.getChildNameByDatatype(myValue.getClass()); encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, false, myParent, false); - managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName); + managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName); } theEventWriter.endObject(); @@ -1438,34 +1437,12 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + value.getClass().getCanonicalName()); } encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true, false, myParent, false); - managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName); + managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName); } // theEventWriter.name(myUndeclaredExtension.get); 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 extensions = new ArrayList(0); - final List modifierExtensions = new ArrayList(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(); - } - } - } - } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index b73802c96c5..7b78de58f85 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -187,6 +187,7 @@ public class Constants { public static final String OO_INFOSTATUS_PROCESSING = "processing"; public static final String PARAM_GRAPHQL_QUERY = "query"; public static final String HEADER_X_CACHE = "X-Cache"; + public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context"; static { CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java new file mode 100644 index 00000000000..b25dcc53f48 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java @@ -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 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); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java index 5ea239ac22e..29d12b36916 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java @@ -20,8 +20,12 @@ package ca.uhn.fhir.jpa.search; * #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.springframework.beans.factory.annotation.Autowired; 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.TransactionTemplate; -import com.google.common.annotations.VisibleForTesting; - -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.data.*; -import ca.uhn.fhir.jpa.entity.Search; +import java.util.Date; /** * Deletes old searches @@ -46,26 +46,21 @@ import ca.uhn.fhir.jpa.entity.Search; public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { 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 Long ourNowForUnitTests; /* * 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 * the result is to be deleted */ private long myCutoffSlack = DEFAULT_CUTOFF_SLACK; - @Autowired private DaoConfig myDaoConfig; - @Autowired private ISearchDao mySearchDao; - @Autowired private ISearchIncludeDao mySearchIncludeDao; - @Autowired private ISearchResultDao mySearchResultDao; - @Autowired private PlatformTransactionManager myTransactionManager; @@ -87,7 +82,7 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) { 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); @@ -131,4 +126,19 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { 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; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index feabf163417..b9d35c68020 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -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(); + } + } + } + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java index c6a6247d36b..ea2fd0c1158 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java @@ -1,12 +1,16 @@ 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.junit.Assert.*; +import ca.uhn.fhir.jpa.util.StopWatch; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.test.util.AopTestUtils; import org.springframework.transaction.TransactionStatus; 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; public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { - @Before - public void beforeDisableResultReuse() { - myDaoConfig.setReuseCachedSearchResultsForMillis(null); - } + private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoDstu3SearchPageExpiryTest.class); @After() public void after() { StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc); staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK); + StaleSearchDeletingSvcImpl.setNowForUnitTests(null); } @Before @@ -35,48 +37,9 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { staleSearchDeletingSvc.setCutoffSlackForUnitTest(0); } - @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); - - 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())); - } - }); + @Before + public void beforeDisableResultReuse() { + myDaoConfig.setReuseCachedSearchResultsForMillis(null); } @Test @@ -98,6 +61,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { myDaoConfig.setExpireSearchResultsAfterMillis(1000L); myDaoConfig.setReuseCachedSearchResultsForMillis(500L); + long start = System.currentTimeMillis(); final String searchUuid1; { @@ -109,7 +73,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { Validate.notBlank(searchUuid1); } - Thread.sleep(250); + sleepAtLeast(250); String searchUuid2; { @@ -122,7 +86,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { } assertEquals(searchUuid1, searchUuid2); - Thread.sleep(500); + sleepAtLeast(500); // 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 - 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(); newTxTemplate().execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theArg0) { - assertNull(mySearchEntityDao.findByUuid(searchUuid1)); 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(); 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())); + } + }); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java index df856260457..aa39d50803a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java @@ -1,33 +1,35 @@ package ca.uhn.fhir.jpa.dao.r4; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.*; - +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; 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.hl7.fhir.r4.model.Patient; 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.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.StringParam; +import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.*; public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { - @Before - public void beforeDisableResultReuse() { - myDaoConfig.setReuseCachedSearchResultsForMillis(null); - } + private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4SearchPageExpiryTest.class); @After() public void after() { StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc); staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK); + StaleSearchDeletingSvcImpl.setNowForUnitTests(null); } @Before @@ -36,50 +38,9 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { staleSearchDeletingSvc.setCutoffSlackForUnitTest(0); } - @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(); - - 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())); - } - }); + @Before + public void beforeDisableResultReuse() { + myDaoConfig.setReuseCachedSearchResultsForMillis(null); } @Test @@ -101,6 +62,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { myDaoConfig.setExpireSearchResultsAfterMillis(1000L); myDaoConfig.setReuseCachedSearchResultsForMillis(500L); + long start = System.currentTimeMillis(); final String searchUuid1; { @@ -112,7 +74,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { Validate.notBlank(searchUuid1); } - Thread.sleep(250); + sleepAtLeast(250); String searchUuid2; { @@ -125,7 +87,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { } assertEquals(searchUuid1, searchUuid2); - Thread.sleep(500); + sleepAtLeast(500); // 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(); newTxTemplate().execute(new TransactionCallbackWithoutResult() { @@ -166,7 +128,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { } }); - Thread.sleep(300); + StaleSearchDeletingSvcImpl.setNowForUnitTests(start + 2200); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); 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())); + } + }); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index a8e7e94a0ed..4f3d920e8a9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -29,6 +29,7 @@ import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.util.BinaryUtil; import org.hl7.fhir.instance.model.api.*; import ca.uhn.fhir.context.FhirContext; @@ -580,10 +581,17 @@ public class RestfulServerUtils { } else { contentType = Constants.CT_OCTET_STREAM; } + // Force binary resources to download - This is a security measure to prevent // malicious images or HTML blocks being served up as content. 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); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 09f8788cc03..894e7285e95 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -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()); - // 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) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java index 03567863f67..de5abc7839c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java @@ -19,7 +19,24 @@ package ca.uhn.fhir.rest.server.method; * limitations under the License. * #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.IOException; @@ -30,27 +47,8 @@ import java.lang.reflect.Modifier; import java.nio.charset.Charset; import java.util.Collection; -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 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; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; public class ResourceParameter implements IParameter { @@ -193,11 +191,22 @@ public class ResourceParameter implements IParameter { String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); if (EncodingEnum.forContentTypeStrict(ct) == null) { FhirContext ctx = theRequest.getServer().getFhirContext(); - IBaseBinary binary = (IBaseBinary) ctx.getResourceDefinition("Binary").newInstance(); + IBaseBinary binary = BinaryUtil.newBinary(ctx); binary.setId(theRequest.getId()); binary.setContentType(ct); binary.setContent(theRequest.loadRequestContents()); 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); + } + } } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java index 0f3507d919a..c8b222d6341 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java @@ -51,14 +51,14 @@ import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.*; public class JsonParserDstu3Test { - private static FhirContext ourCtx = FhirContext.forDstu3(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserDstu3Test.class); + private static FhirContext ourCtx = FhirContext.forDstu3(); @After public void after() { ourCtx.setNarrativeGenerator(null); } - + /** * See #563 @@ -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 */ @@ -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 extlst = fhirPat.getExtensionsByUrl("x1"); + Assert.assertEquals(1, extlst.size()); + Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference()); + } + /** * See #544 */ @@ -271,12 +275,12 @@ public class JsonParserDstu3Test { String enc = ourCtx.newJsonParser().encodeResourceToString(patient); assertThat(enc, Matchers.stringContainsInOrder("{\"resourceType\":\"Patient\",", "\"extension\":[{\"url\":\"http://example.com/extensions#someext\",\"valueDateTime\":\"2011-01-02T11:13:15\"}", - "{\"url\":\"http://example.com#parent\",\"extension\":[{\"url\":\"http://example.com#child\",\"valueString\":\"value1\"},{\"url\":\"http://example.com#child\",\"valueString\":\"value2\"}]}")); + "{\"url\":\"http://example.com#parent\",\"extension\":[{\"url\":\"http://example.com#child\",\"valueString\":\"value1\"},{\"url\":\"http://example.com#child\",\"valueString\":\"value2\"}]}")); assertThat(enc, Matchers.stringContainsInOrder("\"modifierExtension\":[" + "{" + "\"url\":\"http://example.com/extensions#modext\"," + "\"valueDate\":\"1995-01-02\"" + "}" + "],")); assertThat(enc, - containsString("\"_given\":[" + "{" + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext\"," + "\"valueString\":\"given\"" + "}" + "]" + "}," + "{" + "\"extension\":[" + "{" - + "\"url\":\"http://examples.com#givenext_parent\"," + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext_child\"," + "\"valueString\":\"CHILD\"" + "}" + "]" + "}" - + "]" + "}")); + containsString("\"_given\":[" + "{" + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext\"," + "\"valueString\":\"given\"" + "}" + "]" + "}," + "{" + "\"extension\":[" + "{" + + "\"url\":\"http://examples.com#givenext_parent\"," + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext_child\"," + "\"valueString\":\"CHILD\"" + "}" + "]" + "}" + + "]" + "}")); /* * Now parse this back @@ -334,36 +338,36 @@ public class JsonParserDstu3Test { ourLog.info(enc); //@formatter:off - assertThat(enc, stringContainsInOrder("\"meta\": {", - "\"profile\": [", - "\"http://foo/Profile1\",", - "\"http://foo/Profile2\"", - "],", - "\"security\": [", - "{", - "\"system\": \"sec_scheme1\",", - "\"code\": \"sec_term1\",", - "\"display\": \"sec_label1\"", - "},", - "{", - "\"system\": \"sec_scheme2\",", - "\"code\": \"sec_term2\",", - "\"display\": \"sec_label2\"", - "}", - "],", - "\"tag\": [", - "{", - "\"system\": \"scheme1\",", - "\"code\": \"term1\",", - "\"display\": \"label1\"", - "},", - "{", - "\"system\": \"scheme2\",", - "\"code\": \"term2\",", - "\"display\": \"label2\"", - "}", - "]", - "},")); + assertThat(enc, stringContainsInOrder("\"meta\": {", + "\"profile\": [", + "\"http://foo/Profile1\",", + "\"http://foo/Profile2\"", + "],", + "\"security\": [", + "{", + "\"system\": \"sec_scheme1\",", + "\"code\": \"sec_term1\",", + "\"display\": \"sec_label1\"", + "},", + "{", + "\"system\": \"sec_scheme2\",", + "\"code\": \"sec_term2\",", + "\"display\": \"sec_label2\"", + "}", + "],", + "\"tag\": [", + "{", + "\"system\": \"scheme1\",", + "\"code\": \"term1\",", + "\"display\": \"label1\"", + "},", + "{", + "\"system\": \"scheme2\",", + "\"code\": \"term2\",", + "\"display\": \"label2\"", + "}", + "]", + "},")); //@formatter:on Patient parsed = ourCtx.newJsonParser().parseResource(Patient.class, enc); @@ -462,29 +466,29 @@ public class JsonParserDstu3Test { ourLog.info(enc); //@formatter:off - assertEquals("{\n" + - " \"resourceType\": \"Patient\",\n" + - " \"meta\": {\n" + - " \"security\": [\n" + - " {\n" + - " \"system\": \"SYSTEM1\",\n" + - " \"version\": \"VERSION1\",\n" + - " \"code\": \"CODE1\",\n" + - " \"display\": \"DISPLAY1\"\n" + - " },\n" + - " {\n" + - " \"system\": \"SYSTEM2\",\n" + - " \"version\": \"VERSION2\",\n" + - " \"code\": \"CODE2\",\n" + - " \"display\": \"DISPLAY2\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"name\": [\n" + - " {\n" + - " \"family\": \"FAMILY\"\n" + - " }\n" + - " ]\n" + + assertEquals("{\n" + + " \"resourceType\": \"Patient\",\n" + + " \"meta\": {\n" + + " \"security\": [\n" + + " {\n" + + " \"system\": \"SYSTEM1\",\n" + + " \"version\": \"VERSION1\",\n" + + " \"code\": \"CODE1\",\n" + + " \"display\": \"DISPLAY1\"\n" + + " },\n" + + " {\n" + + " \"system\": \"SYSTEM2\",\n" + + " \"version\": \"VERSION2\",\n" + + " \"code\": \"CODE2\",\n" + + " \"display\": \"DISPLAY2\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"name\": [\n" + + " {\n" + + " \"family\": \"FAMILY\"\n" + + " }\n" + + " ]\n" + "}", enc.trim()); //@formatter:on @@ -506,6 +510,22 @@ public class JsonParserDstu3Test { 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 public void testEncodeBundleNewBundleNoText() { @@ -539,22 +559,22 @@ public class JsonParserDstu3Test { //@formatter:off assertThat(encoded, stringContainsInOrder( "{", - "\"resourceType\": \"Patient\",", - "\"contained\": [", - "{", - "\"resourceType\": \"Condition\",", - "\"id\": \"1\"", - "}", - "],", - "\"extension\": [", - "{", - "\"url\": \"test\",", - "\"valueReference\": {", - "\"reference\": \"#1\"", - "}", - "}", - "],", - "\"birthDate\": \"2016-04-05\"", + "\"resourceType\": \"Patient\",", + "\"contained\": [", + "{", + "\"resourceType\": \"Condition\",", + "\"id\": \"1\"", + "}", + "],", + "\"extension\": [", + "{", + "\"url\": \"test\",", + "\"valueReference\": {", + "\"reference\": \"#1\"", + "}", + "}", + "],", + "\"birthDate\": \"2016-04-05\"", "}" )); //@formatter:on @@ -575,23 +595,6 @@ public class JsonParserDstu3Test { String output = ourCtx.newJsonParser().encodeResourceToString(new Binary()); 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 @@ -663,8 +666,8 @@ public class JsonParserDstu3Test { ourLog.info(val); assertEquals( - "{\"resourceType\":\"Patient\",\"id\":\"123\",\"contact\":[{\"extension\":[{\"url\":\"http://foo.com/contact-eyecolour\",\"valueIdentifier\":{\"value\":\"EYE\"}}],\"name\":{\"family\":\"FAMILY\"}}]}", - val); + "{\"resourceType\":\"Patient\",\"id\":\"123\",\"contact\":[{\"extension\":[{\"url\":\"http://foo.com/contact-eyecolour\",\"valueIdentifier\":{\"value\":\"EYE\"}}],\"name\":{\"family\":\"FAMILY\"}}]}", + val); FhirContext newCtx = FhirContext.forDstu3(); PatientWithExtendedContactDstu3 actual = newCtx.newJsonParser().parseResource(PatientWithExtendedContactDstu3.class, val); @@ -706,9 +709,9 @@ public class JsonParserDstu3Test { Patient p = new Patient(); p.setId("Patient/B"); p - .addExtension() - .setUrl("http://foo") - .setValue(new Reference("Practitioner/A")); + .addExtension() + .setUrl("http://foo") + .setValue(new Reference("Practitioner/A")); IParser parser = ourCtx.newJsonParser().setPrettyPrint(true); parser.setDontEncodeElements(new HashSet(Arrays.asList("*.id", "*.meta"))); @@ -777,20 +780,20 @@ public class JsonParserDstu3Test { //@formatter:off assertThat(output, stringContainsInOrder( - "\"id\": \"1\"", - "\"meta\"", - "\"extension\"", - "\"url\": \"http://exturl\"", - "\"extension\"", - "\"url\": \"http://subext\"", - "\"valueString\": \"sub_ext_value\"", - "\"code\":" - )); - assertThat(output, not(stringContainsInOrder( - "\"url\": \"http://exturl\"", - ",", - "\"url\": \"http://exturl\"" - ))); + "\"id\": \"1\"", + "\"meta\"", + "\"extension\"", + "\"url\": \"http://exturl\"", + "\"extension\"", + "\"url\": \"http://subext\"", + "\"valueString\": \"sub_ext_value\"", + "\"code\":" + )); + assertThat(output, not(stringContainsInOrder( + "\"url\": \"http://exturl\"", + ",", + "\"url\": \"http://exturl\"" + ))); //@formatter:on obs = parser.parseResource(Observation.class, output); @@ -821,17 +824,17 @@ public class JsonParserDstu3Test { //@formatter:off assertThat(encoded, stringContainsInOrder( - "\"resourceType\": \"Patient\"", + "\"resourceType\": \"Patient\"", "\"contained\": [", - "\"resourceType\": \"Condition\"", - "\"id\": \"1\"", - "\"bodySite\": [", - "\"text\": \"BODY SITE\"", - "\"extension\": [", - "\"url\": \"testCondition\",", - "\"valueReference\": {", - "\"reference\": \"#1\"", - "\"birthDate\": \"2016-04-14\"", + "\"resourceType\": \"Condition\"", + "\"id\": \"1\"", + "\"bodySite\": [", + "\"text\": \"BODY SITE\"", + "\"extension\": [", + "\"url\": \"testCondition\",", + "\"valueReference\": {", + "\"reference\": \"#1\"", + "\"birthDate\": \"2016-04-14\"", "}" )); //@formatter:on @@ -1071,7 +1074,7 @@ public class JsonParserDstu3Test { assertThat(encoded, containsString("Patient")); assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"foo\",", "\"code\": \"bar\"", "\"system\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_SYSTEM + "\"", - "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); + "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); assertThat(encoded, not(containsString("THE DIV"))); assertThat(encoded, containsString("family")); assertThat(encoded, not(containsString("maritalStatus"))); @@ -1091,7 +1094,7 @@ public class JsonParserDstu3Test { ourLog.info(enc); assertEquals("{\"resourceType\":\"Patient\",\"meta\":{\"tag\":[{\"system\":\"scheme\",\"code\":\"term\",\"display\":\"display\"}]},\"identifier\":[{\"system\":\"sys\",\"value\":\"val\"}]}", - enc); + enc); } @@ -1268,19 +1271,19 @@ public class JsonParserDstu3Test { @Test public void testExplanationOfBenefit() { //@formatter:off - String input = "{" + - " \"resourceType\": \"ExplanationOfBenefit\"," + - " \"insurance\": {\n" + - " \"coverage\": {\n" + - " \"reference\": \"Coverage/123\"\n" + - " }\n" + - " },\n" + - " \"relationship\": {\n" + - " \"system\": \"http://hl7.org/fhir/relationship\",\n" + - " \"code\": \"1\",\n" + - " \"display\": \"self\"\n" + - " }\n" + - "}"; + String input = "{" + + " \"resourceType\": \"ExplanationOfBenefit\"," + + " \"insurance\": {\n" + + " \"coverage\": {\n" + + " \"reference\": \"Coverage/123\"\n" + + " }\n" + + " },\n" + + " \"relationship\": {\n" + + " \"system\": \"http://hl7.org/fhir/relationship\",\n" + + " \"code\": \"1\",\n" + + " \"display\": \"self\"\n" + + " }\n" + + "}"; //@formatter:on ExplanationOfBenefit eob = ourCtx.newJsonParser().parseResource(ExplanationOfBenefit.class, input); @@ -1334,14 +1337,14 @@ public class JsonParserDstu3Test { // ID should be a String and communication should be an Array String input = "{\"resourceType\": \"Patient\",\n" + - " \"id\": 123,\n" + - " \"communication\": {\n" + - " \"language\": {\n" + - " \"text\": \"Hindi\"\n" + - " },\n" + - " \"preferred\": true\n" + - " }\n" + - "}"; + " \"id\": 123,\n" + + " \"communication\": {\n" + + " \"language\": {\n" + + " \"text\": \"Hindi\"\n" + + " },\n" + + " \"preferred\": true\n" + + " }\n" + + "}"; IParser p = ourCtx.newJsonParser(); @@ -1375,14 +1378,14 @@ public class JsonParserDstu3Test { // ID should be a String and communication should be an Array String input = "{\"resourceType\": \"Patient\",\n" + - " \"id\": \"123\",\n" + - " \"communication\": [{\n" + - " \"language\": {\n" + - " \"text\": \"Hindi\"\n" + - " },\n" + - " \"preferred\": true\n" + - " }]\n" + - "}"; + " \"id\": \"123\",\n" + + " \"communication\": [{\n" + + " \"language\": {\n" + + " \"text\": \"Hindi\"\n" + + " },\n" + + " \"preferred\": true\n" + + " }]\n" + + "}"; IParser p = ourCtx.newJsonParser(); @@ -1503,14 +1506,14 @@ public class JsonParserDstu3Test { @Ignore public void testNamespacePreservationEncode() throws Exception { //@formatter:off - String input = "" + - "" + - "" + - "" + - "@fhirabend" + - "" + - "" + - ""; + String input = "" + + "" + + "" + + "" + + "@fhirabend" + + "" + + "" + + ""; //@formatter:on Patient parsed = ourCtx.newXmlParser().parseResource(Patient.class, input); @@ -1712,69 +1715,69 @@ public class JsonParserDstu3Test { @Test public void testParseAndEncodeBundleWithUuidBase() { //@formatter:off - String input = - "{\n" + - " \"resourceType\":\"Bundle\",\n" + - " \"type\":\"document\",\n" + - " \"entry\":[\n" + - " {\n" + - " \"fullUrl\":\"urn:uuid:180f219f-97a8-486d-99d9-ed631fe4fc57\",\n" + - " \"resource\":{\n" + - " \"resourceType\":\"Composition\",\n" + - " \"id\":\"180f219f-97a8-486d-99d9-ed631fe4fc57\",\n" + - " \"meta\":{\n" + - " \"lastUpdated\":\"2013-05-28T22:12:21Z\"\n" + - " },\n" + - " \"text\":{\n" + - " \"status\":\"generated\",\n" + - " \"div\":\"

Generated Narrative with Details

id: 180f219f-97a8-486d-99d9-ed631fe4fc57

meta:

date: Feb 1, 2013 12:30:02 PM

type: Discharge Summary from Responsible Clinician (Details : {LOINC code '28655-9' = 'Physician attending Discharge summary)

status: final

confidentiality: N

author: Doctor Dave. Generated Summary: 23; Adam Careful

encounter: http://fhir.healthintersections.com.au/open/Encounter/doc-example

\"\n" + - " },\n" + - " \"date\":\"2013-02-01T12:30:02Z\",\n" + - " \"type\":{\n" + - " \"coding\":[\n" + - " {\n" + - " \"system\":\"http://loinc.org\",\n" + - " \"code\":\"28655-9\"\n" + - " }\n" + - " ],\n" + - " \"text\":\"Discharge Summary from Responsible Clinician\"\n" + - " },\n" + - " \"status\":\"final\",\n" + - " \"confidentiality\":\"N\",\n" + - " \"subject\":{\n" + - " \"reference\":\"http://fhir.healthintersections.com.au/open/Patient/d1\",\n" + - " \"display\":\"Eve Everywoman\"\n" + - " },\n" + - " \"author\":[\n" + - " {\n" + - " \"reference\":\"Practitioner/example\",\n" + - " \"display\":\"Doctor Dave\"\n" + - " }\n" + - " ],\n" + - " \"encounter\":{\n" + - " \"reference\":\"http://fhir.healthintersections.com.au/open/Encounter/doc-example\"\n" + - " },\n" + - " \"section\":[\n" + - " {\n" + - " \"title\":\"Reason for admission\",\n" + - " \"content\":{\n" + - " \"reference\":\"urn:uuid:d0dd51d3-3ab2-4c84-b697-a630c3e40e7a\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"title\":\"Medications on Discharge\",\n" + - " \"content\":{\n" + - " \"reference\":\"urn:uuid:673f8db5-0ffd-4395-9657-6da00420bbc1\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"title\":\"Known allergies\",\n" + - " \"content\":{\n" + - " \"reference\":\"urn:uuid:68f86194-e6e1-4f65-b64a-5314256f8d7b\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + + String input = + "{\n" + + " \"resourceType\":\"Bundle\",\n" + + " \"type\":\"document\",\n" + + " \"entry\":[\n" + + " {\n" + + " \"fullUrl\":\"urn:uuid:180f219f-97a8-486d-99d9-ed631fe4fc57\",\n" + + " \"resource\":{\n" + + " \"resourceType\":\"Composition\",\n" + + " \"id\":\"180f219f-97a8-486d-99d9-ed631fe4fc57\",\n" + + " \"meta\":{\n" + + " \"lastUpdated\":\"2013-05-28T22:12:21Z\"\n" + + " },\n" + + " \"text\":{\n" + + " \"status\":\"generated\",\n" + + " \"div\":\"

Generated Narrative with Details

id: 180f219f-97a8-486d-99d9-ed631fe4fc57

meta:

date: Feb 1, 2013 12:30:02 PM

type: Discharge Summary from Responsible Clinician (Details : {LOINC code '28655-9' = 'Physician attending Discharge summary)

status: final

confidentiality: N

author: Doctor Dave. Generated Summary: 23; Adam Careful

encounter: http://fhir.healthintersections.com.au/open/Encounter/doc-example

\"\n" + + " },\n" + + " \"date\":\"2013-02-01T12:30:02Z\",\n" + + " \"type\":{\n" + + " \"coding\":[\n" + + " {\n" + + " \"system\":\"http://loinc.org\",\n" + + " \"code\":\"28655-9\"\n" + + " }\n" + + " ],\n" + + " \"text\":\"Discharge Summary from Responsible Clinician\"\n" + + " },\n" + + " \"status\":\"final\",\n" + + " \"confidentiality\":\"N\",\n" + + " \"subject\":{\n" + + " \"reference\":\"http://fhir.healthintersections.com.au/open/Patient/d1\",\n" + + " \"display\":\"Eve Everywoman\"\n" + + " },\n" + + " \"author\":[\n" + + " {\n" + + " \"reference\":\"Practitioner/example\",\n" + + " \"display\":\"Doctor Dave\"\n" + + " }\n" + + " ],\n" + + " \"encounter\":{\n" + + " \"reference\":\"http://fhir.healthintersections.com.au/open/Encounter/doc-example\"\n" + + " },\n" + + " \"section\":[\n" + + " {\n" + + " \"title\":\"Reason for admission\",\n" + + " \"content\":{\n" + + " \"reference\":\"urn:uuid:d0dd51d3-3ab2-4c84-b697-a630c3e40e7a\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"title\":\"Medications on Discharge\",\n" + + " \"content\":{\n" + + " \"reference\":\"urn:uuid:673f8db5-0ffd-4395-9657-6da00420bbc1\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"title\":\"Known allergies\",\n" + + " \"content\":{\n" + + " \"reference\":\"urn:uuid:68f86194-e6e1-4f65-b64a-5314256f8d7b\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + " }" + " ]" + "}"; @@ -1794,34 +1797,34 @@ public class JsonParserDstu3Test { @Test public void testParseAndEncodeComments() { //@formatter:off - String input = "{\n" + - " \"resourceType\": \"Patient\",\n" + - " \"id\": \"pat1\",\n" + - " \"text\": {\n" + - " \"status\": \"generated\",\n" + - " \"div\": \"
\\n \\n

Patient Donald DUCK @ Acme Healthcare, Inc. MR = 654321

\\n \\n
\"\n" + - " },\n" + - " \"identifier\": [\n" + - " {\n" + - " \"fhir_comments\":[\"identifier comment 1\",\"identifier comment 2\"],\n" + - " \"use\": \"usual\",\n" + - " \"_use\": {\n" + - " \"fhir_comments\":[\"use comment 1\",\"use comment 2\"]\n" + - " },\n" + - " \"type\": {\n" + - " \"coding\": [\n" + - " {\n" + - " \"system\": \"http://hl7.org/fhir/v2/0203\",\n" + - " \"code\": \"MR\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"system\": \"urn:oid:0.1.2.3.4.5.6.7\",\n" + - " \"value\": \"654321\"\n" + - " }\n" + - " ],\n" + - " \"active\": true" + - "}"; + String input = "{\n" + + " \"resourceType\": \"Patient\",\n" + + " \"id\": \"pat1\",\n" + + " \"text\": {\n" + + " \"status\": \"generated\",\n" + + " \"div\": \"
\\n \\n

Patient Donald DUCK @ Acme Healthcare, Inc. MR = 654321

\\n \\n
\"\n" + + " },\n" + + " \"identifier\": [\n" + + " {\n" + + " \"fhir_comments\":[\"identifier comment 1\",\"identifier comment 2\"],\n" + + " \"use\": \"usual\",\n" + + " \"_use\": {\n" + + " \"fhir_comments\":[\"use comment 1\",\"use comment 2\"]\n" + + " },\n" + + " \"type\": {\n" + + " \"coding\": [\n" + + " {\n" + + " \"system\": \"http://hl7.org/fhir/v2/0203\",\n" + + " \"code\": \"MR\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"system\": \"urn:oid:0.1.2.3.4.5.6.7\",\n" + + " \"value\": \"654321\"\n" + + " }\n" + + " ],\n" + + " \"active\": true" + + "}"; //@formatter:off Patient res = ourCtx.newJsonParser().parseResource(Patient.class, input); @@ -1829,33 +1832,33 @@ public class JsonParserDstu3Test { assertEquals("Patient/pat1", res.getId()); assertEquals("654321", res.getIdentifier().get(0).getValue()); assertEquals(true, res.getActive()); - + assertThat(res.getIdentifier().get(0).getFormatCommentsPre(), contains("identifier comment 1", "identifier comment 2")); assertThat(res.getIdentifier().get(0).getUseElement().getFormatCommentsPre(), contains("use comment 1", "use comment 2")); - + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(res); ourLog.info(encoded); - + //@formatter:off assertThat(encoded, stringContainsInOrder( - "\"identifier\": [", - "{", - "\"fhir_comments\":", - "[", - "\"identifier comment 1\"", - ",", - "\"identifier comment 2\"", - "]", - "\"use\": \"usual\",", - "\"_use\": {", - "\"fhir_comments\":", - "[", - "\"use comment 1\"", - ",", - "\"use comment 2\"", - "]", - "},", - "\"type\"" + "\"identifier\": [", + "{", + "\"fhir_comments\":", + "[", + "\"identifier comment 1\"", + ",", + "\"identifier comment 2\"", + "]", + "\"use\": \"usual\",", + "\"_use\": {", + "\"fhir_comments\":", + "[", + "\"use comment 1\"", + ",", + "\"use comment 2\"", + "]", + "},", + "\"type\"" )); //@formatter:off } @@ -1865,7 +1868,7 @@ public class JsonParserDstu3Test { Binary patient = new Binary(); patient.setId(new IdType("http://base/Binary/11/_history/22")); 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); @@ -1916,7 +1919,7 @@ public class JsonParserDstu3Test { "\"resourceType\":\"Patient\",", "\"id\":\"1\"", "\"reference\":\"#1\"" - )); + )); //@formatter:on o = parser.parseResource(Observation.class, enc); @@ -1927,6 +1930,21 @@ public class JsonParserDstu3Test { 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 */ @@ -1987,10 +2005,10 @@ public class JsonParserDstu3Test { @Test public void testParseJsonExtensionWithoutUrl() { //@formatter:off - String input = + String input = "{\"resourceType\":\"Patient\"," + - "\"extension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + - "}"; + "\"extension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + + "}"; //@formatter:on IParser parser = ourCtx.newJsonParser(); @@ -2017,10 +2035,10 @@ public class JsonParserDstu3Test { @Test public void testParseJsonModifierExtensionWithoutUrl() { //@formatter:off - String input = + String input = "{\"resourceType\":\"Patient\"," + - "\"modifierExtension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + - "}"; + "\"modifierExtension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + + "}"; //@formatter:on IParser parser = ourCtx.newJsonParser(); @@ -2044,24 +2062,24 @@ public class JsonParserDstu3Test { @Test public void testParseMetadata() throws Exception { //@formatter:off - String bundle = "{\n" + - " \"resourceType\" : \"Bundle\",\n" + - " \"total\" : 1,\n" + - " \"link\": [{\n" + - " \"relation\" : \"self\",\n" + - " \"url\" : \"http://localhost:52788/Binary?_pretty=true\"\n" + - " }],\n" + - " \"entry\" : [{\n" + + String bundle = "{\n" + + " \"resourceType\" : \"Bundle\",\n" + + " \"total\" : 1,\n" + + " \"link\": [{\n" + + " \"relation\" : \"self\",\n" + + " \"url\" : \"http://localhost:52788/Binary?_pretty=true\"\n" + + " }],\n" + + " \"entry\" : [{\n" + " \"fullUrl\" : \"http://foo/fhirBase2/Patient/1/_history/2\",\n" + - " \"resource\" : {\n" + - " \"resourceType\" : \"Patient\",\n" + - " \"id\" : \"1\",\n" + + " \"resource\" : {\n" + + " \"resourceType\" : \"Patient\",\n" + + " \"id\" : \"1\",\n" + " \"meta\" : {\n" + " \"versionId\" : \"2\",\n" + " \"lastUpdated\" : \"2001-02-22T11:22:33-05:00\"\n" + - " },\n" + - " \"birthDate\" : \"2012-01-02\"\n" + - " },\n" + + " },\n" + + " \"birthDate\" : \"2012-01-02\"\n" + + " },\n" + " \"search\" : {\n" + " \"mode\" : \"match\",\n" + " \"score\" : 0.123\n" + @@ -2070,7 +2088,7 @@ public class JsonParserDstu3Test { " \"method\" : \"POST\",\n" + " \"url\" : \"http://foo/Patient?identifier=value\"\n" + " }\n" + - " }]\n" + + " }]\n" + "}"; //@formatter:on @@ -2182,6 +2200,43 @@ public class JsonParserDstu3Test { } } + @Test + public void testParseWithPrecision() { + String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}"; + Observation obs = ourCtx.newJsonParser().parseResource(Observation.class, input); + + DecimalType valueElement = ((Quantity) obs.getValue()).getValueElement(); + assertEquals("0.000000000000000100", valueElement.getValueAsString()); + + String str = ourCtx.newJsonParser().encodeResourceToString(obs); + ourLog.info(str); + assertEquals("{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}", str); + } + + @Test(expected = DataFormatException.class) + public void testParseWithTrailingContent() throws Exception { + //@formatter:off + String bundle = "{\n" + + " \"resourceType\" : \"Bundle\",\n" + + " \"total\" : 1\n" + + "}}"; + //@formatter:on + + Bundle b = ourCtx.newJsonParser().parseResource(Bundle.class, bundle); + } + + @Test + @Ignore + public void testParseWithWrongTypeObjectShouldBeArray() throws Exception { + String input = IOUtils.toString(getClass().getResourceAsStream("/invalid_metadata.json")); + try { + ourCtx.newJsonParser().parseResource(CapabilityStatement.class, input); + fail(); + } catch (DataFormatException e) { + assertEquals("Syntax error parsing JSON FHIR structure: Expected ARRAY at element 'modifierExtension', found 'OBJECT'", e.getMessage()); + } + } + /** * See #344 */ @@ -2215,43 +2270,6 @@ public class JsonParserDstu3Test { } } - @Test - public void testParseWithPrecision() { - String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}"; - Observation obs = ourCtx.newJsonParser().parseResource(Observation.class, input); - - DecimalType valueElement = ((Quantity) obs.getValue()).getValueElement(); - assertEquals("0.000000000000000100", valueElement.getValueAsString()); - - String str = ourCtx.newJsonParser().encodeResourceToString(obs); - ourLog.info(str); - assertEquals("{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}", str); - } - - @Test(expected = DataFormatException.class) - public void testParseWithTrailingContent() throws Exception { - //@formatter:off - String bundle = "{\n" + - " \"resourceType\" : \"Bundle\",\n" + - " \"total\" : 1\n" + - "}}"; - //@formatter:on - - Bundle b = ourCtx.newJsonParser().parseResource(Bundle.class, bundle); - } - - @Test - @Ignore - public void testParseWithWrongTypeObjectShouldBeArray() throws Exception { - String input = IOUtils.toString(getClass().getResourceAsStream("/invalid_metadata.json")); - try { - ourCtx.newJsonParser().parseResource(CapabilityStatement.class, input); - fail(); - } catch (DataFormatException e) { - assertEquals("Syntax error parsing JSON FHIR structure: Expected ARRAY at element 'modifierExtension', found 'OBJECT'", e.getMessage()); - } - } - /** * See #144 and #146 */ @@ -2323,13 +2341,13 @@ public class JsonParserDstu3Test { ArgumentCaptor expectedScalar = ArgumentCaptor.forClass(ScalarType.class); ArgumentCaptor actualScalar = ArgumentCaptor.forClass(ScalarType.class); verify(errorHandler, atLeastOnce()).incorrectJsonType(Mockito.any(IParseLocation.class), elementName.capture(), expected.capture(), expectedScalar.capture(), actual.capture(), - actualScalar.capture()); + actualScalar.capture()); verify(errorHandler, atLeastOnce()).incorrectJsonType(Mockito.any(IParseLocation.class), Mockito.eq("_id"), Mockito.eq(ValueType.OBJECT), expectedScalar.capture(), Mockito.eq(ValueType.SCALAR), - actualScalar.capture()); + actualScalar.capture()); verify(errorHandler, atLeastOnce()).incorrectJsonType(Mockito.any(IParseLocation.class), Mockito.eq("__v"), Mockito.eq(ValueType.OBJECT), expectedScalar.capture(), Mockito.eq(ValueType.SCALAR), - actualScalar.capture()); + actualScalar.capture()); verify(errorHandler, atLeastOnce()).incorrectJsonType(Mockito.any(IParseLocation.class), Mockito.eq("_status"), Mockito.eq(ValueType.OBJECT), expectedScalar.capture(), - Mockito.eq(ValueType.SCALAR), actualScalar.capture()); + Mockito.eq(ValueType.SCALAR), actualScalar.capture()); assertEquals("_id", elementName.getAllValues().get(0)); assertEquals(ValueType.OBJECT, expected.getAllValues().get(0)); @@ -2361,26 +2379,6 @@ public class JsonParserDstu3Test { 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 extlst = fhirPat.getExtensionsByUrl("x1"); - Assert.assertEquals(1, extlst.size()); - Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference()); - } - @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java index 4c63567c71f..fe8a29f4f42 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java @@ -82,6 +82,23 @@ public class XmlParserDstu3Test { 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("")); + assertThat(encoded, containsString("")); + assertThat(encoded, containsString("")); + } + + /** * See #544 */ diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/BinaryServerR4Test.java similarity index 50% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryR4Test.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/BinaryServerR4Test.java index 51d2abef86f..4bee8510f9e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/BinaryServerR4Test.java @@ -1,14 +1,16 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.util.concurrent.TimeUnit; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.*; +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; import org.apache.commons.io.IOUtils; 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.HttpPut; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; 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.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.*; -import org.junit.*; +import org.hl7.fhir.r4.model.Binary; +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 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; +import java.util.concurrent.TimeUnit; -public class CreateBinaryR4Test { +import static org.junit.Assert.*; + +public class BinaryServerR4Test { private static CloseableHttpClient ourClient; private static FhirContext ourCtx = FhirContext.forR4(); private static Binary ourLastBinary; @@ -37,24 +41,73 @@ public class CreateBinaryR4Test { private static String ourLastBinaryString; private static int ourPort; private static Server ourServer; + private static IdType ourLastId; + private static Binary ourNextBinary; @Before public void before() { ourLastBinary = null; ourLastBinaryBytes = null; ourLastBinaryString = null; + ourLastId = null; + ourNextBinary = null; } @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"); - 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"); CloseableHttpResponse status = ourClient.execute(post); try { + assertNull(ourLastId); assertEquals("application/foo", ourLastBinary.getContentType()); - 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}, ourLastBinary.getContent()); + assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes); } finally { 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 */ @Test - public void testRawBytesFhirContentType() throws Exception { + public void testPostRawBytesFhirContentType() throws Exception { Binary b = new Binary(); 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); HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary"); @@ -77,14 +130,14 @@ public class CreateBinaryR4Test { CloseableHttpResponse status = ourClient.execute(post); try { 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 { IOUtils.closeQuietly(status); } } @Test - public void testRawBytesFhirContentTypeContainingFhir() throws Exception { + public void testPostRawBytesFhirContentTypeContainingFhir() throws Exception { Patient p = new Patient(); p.getText().setDivAsString("A PATIENT"); @@ -109,13 +162,32 @@ public class CreateBinaryR4Test { } @Test - public void testRawBytesNoContentType() throws Exception { + public void testPostRawBytesNoContentType() throws Exception { 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); try { 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 { IOUtils.closeQuietly(status); } @@ -149,7 +221,6 @@ public class CreateBinaryR4Test { } public static class BinaryProvider implements IResourceProvider { - @Create() public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) { ourLastBinary = theBinary; @@ -163,6 +234,20 @@ public class CreateBinaryR4Test { 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")); + } + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 046692dc885..0c3ed55d06b 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -175,6 +175,11 @@ was not encoded correctly. Thanks to Malcolm McRoberts for the pull request with fix and test case! + + 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). +