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

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

View File

@ -118,6 +118,7 @@
</ignoredDependencies> </ignoredDependencies>
<ignoredResources> <ignoredResources>
<ignoredResource>changelog.txt</ignoredResource> <ignoredResource>changelog.txt</ignoredResource>
<ignoredResource>javac.bat</ignoredResource>
</ignoredResources> </ignoredResources>
</configuration> </configuration>
</plugin> </plugin>

View File

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

View File

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

View File

@ -187,6 +187,7 @@ public class Constants {
public static final String OO_INFOSTATUS_PROCESSING = "processing"; public static final String OO_INFOSTATUS_PROCESSING = "processing";
public static final String PARAM_GRAPHQL_QUERY = "query"; public static final String PARAM_GRAPHQL_QUERY = "query";
public static final String HEADER_X_CACHE = "X-Cache"; public static final String HEADER_X_CACHE = "X-Cache";
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
static { static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.util.BinaryUtil;
import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -580,10 +581,17 @@ public class RestfulServerUtils {
} else { } else {
contentType = Constants.CT_OCTET_STREAM; contentType = Constants.CT_OCTET_STREAM;
} }
// Force binary resources to download - This is a security measure to prevent // Force binary resources to download - This is a security measure to prevent
// malicious images or HTML blocks being served up as content. // malicious images or HTML blocks being served up as content.
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
IBaseReference securityContext = BinaryUtil.getSecurityContext(theServer.getFhirContext(), bin);
String securityContextRef = securityContext.getReferenceElement().getValue();
if (isNotBlank(securityContextRef)) {
response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, securityContextRef);
}
return response.sendAttachmentResponse(bin, theStausCode, contentType); return response.sendAttachmentResponse(bin, theStausCode, contentType);
} }

View File

@ -149,24 +149,6 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
return theRequest.getResponse().streamResponseAsResource(response, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), isAddContentLocationHeader()); return theRequest.getResponse().streamResponseAsResource(response, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), isAddContentLocationHeader());
// DSTU1 Bundle
// // Is this request coming from a browser
// String uaHeader = theRequest.getHeader("user-agent");
// boolean requestIsBrowser = false;
// if (uaHeader != null && uaHeader.contains("Mozilla")) {
// requestIsBrowser = true;
// }
//
// for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
// IServerInterceptor next = theServer.getInterceptors().get(i);
// boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle());
// if (!continueProcessing) {
// ourLog.debug("Interceptor {} returned false, not continuing processing");
// return null;
// }
// }
//
// return theRequest.getResponse().streamResponseAsBundle(responseObject.getDstu1Bundle(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser);
} }
public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {

View File

@ -19,7 +19,24 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.BinaryUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -30,27 +47,8 @@ import java.lang.reflect.Modifier;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collection; import java.util.Collection;
import org.apache.commons.io.IOUtils; import static org.apache.commons.lang3.StringUtils.isBlank;
import org.apache.commons.lang3.Validate; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class ResourceParameter implements IParameter { public class ResourceParameter implements IParameter {
@ -193,11 +191,22 @@ public class ResourceParameter implements IParameter {
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (EncodingEnum.forContentTypeStrict(ct) == null) { if (EncodingEnum.forContentTypeStrict(ct) == null) {
FhirContext ctx = theRequest.getServer().getFhirContext(); FhirContext ctx = theRequest.getServer().getFhirContext();
IBaseBinary binary = (IBaseBinary) ctx.getResourceDefinition("Binary").newInstance(); IBaseBinary binary = BinaryUtil.newBinary(ctx);
binary.setId(theRequest.getId()); binary.setId(theRequest.getId());
binary.setContentType(ct); binary.setContentType(ct);
binary.setContent(theRequest.loadRequestContents()); binary.setContent(theRequest.loadRequestContents());
retVal = binary; retVal = binary;
/*
* Security context header, which is only in
* DSTU3+
*/
if (ctx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
String securityContext = theRequest.getHeader(Constants.HEADER_X_SECURITY_CONTEXT);
if (isNotBlank(securityContext)) {
BinaryUtil.setSecurityContext(ctx, binary, securityContext);
}
}
} }
} }

View File

@ -51,8 +51,8 @@ import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class JsonParserDstu3Test { public class JsonParserDstu3Test {
private static FhirContext ourCtx = FhirContext.forDstu3();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserDstu3Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserDstu3Test.class);
private static FhirContext ourCtx = FhirContext.forDstu3();
@After @After
public void after() { public void after() {
@ -77,22 +77,6 @@ public class JsonParserDstu3Test {
} }
} }
/**
* See #720
*/
@Test
public void testParseCustomResourceType() {
String input = "{\"resourceType\":\"Bug720ResourceType\",\"meta\":{\"profile\":[\"http://example.com/StructureDefinition/dontuse#Bug720ResourceType\"]},\"supportedVersion\":\"2.5.x\",\"templatesConsentTemplate\":[{\"domainName\":\"name\",\"Name\":\"template_01\",\"version\":\"1.0\",\"title\":\"title\",\"comment\":\"comment\",\"contact\":{\"resourceType\":\"Person\",\"name\":[{\"family\":\"Mustermann\",\"given\":[\"Max\"]}],\"telecom\":[{\"system\":\"email\",\"value\":\"max.mustermann@mail.de\"},{\"system\":\"phone\",\"value\":\"+49 1234 23232\"}],\"address\":[{\"text\":\"street 1-2\",\"city\":\"city\",\"postalCode\":\"12345\",\"country\":\"Germany\"}]}}]}";
Bug720ResourceType parsed = ourCtx.newJsonParser().parseResource(Bug720ResourceType.class, input);
assertEquals(1, parsed.getTemplates().size());
assertEquals(Bug720Datatype.class, parsed.getTemplates().get(0).getClass());
assertEquals("Mustermann", ((Bug720Datatype)parsed.getTemplates().get(0)).getContact().getNameFirstRep().getFamily());
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(parsed));
}
/** /**
* See #563 * See #563
*/ */
@ -110,6 +94,26 @@ public class JsonParserDstu3Test {
} }
} }
@Test
public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
String refVal = "http://my.org/FooBar";
Patient fhirPat = new Patient();
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
IParser parser = ourCtx.newJsonParser();
String output = parser.encodeResourceToString(fhirPat);
System.out.println("output: " + output);
// Deserialize then check that valueReference value is still correct
fhirPat = parser.parseResource(Patient.class, output);
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
Assert.assertEquals(1, extlst.size());
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
}
/** /**
* See #544 * See #544
*/ */
@ -271,12 +275,12 @@ public class JsonParserDstu3Test {
String enc = ourCtx.newJsonParser().encodeResourceToString(patient); 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\"}", 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, Matchers.stringContainsInOrder("\"modifierExtension\":[" + "{" + "\"url\":\"http://example.com/extensions#modext\"," + "\"valueDate\":\"1995-01-02\"" + "}" + "],"));
assertThat(enc, assertThat(enc,
containsString("\"_given\":[" + "{" + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext\"," + "\"valueString\":\"given\"" + "}" + "]" + "}," + "{" + "\"extension\":[" + "{" 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\"" + "}" + "]" + "}" + "\"url\":\"http://examples.com#givenext_parent\"," + "\"extension\":[" + "{" + "\"url\":\"http://examples.com#givenext_child\"," + "\"valueString\":\"CHILD\"" + "}" + "]" + "}"
+ "]" + "}")); + "]" + "}"));
/* /*
* Now parse this back * Now parse this back
@ -335,35 +339,35 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
assertThat(enc, stringContainsInOrder("\"meta\": {", assertThat(enc, stringContainsInOrder("\"meta\": {",
"\"profile\": [", "\"profile\": [",
"\"http://foo/Profile1\",", "\"http://foo/Profile1\",",
"\"http://foo/Profile2\"", "\"http://foo/Profile2\"",
"],", "],",
"\"security\": [", "\"security\": [",
"{", "{",
"\"system\": \"sec_scheme1\",", "\"system\": \"sec_scheme1\",",
"\"code\": \"sec_term1\",", "\"code\": \"sec_term1\",",
"\"display\": \"sec_label1\"", "\"display\": \"sec_label1\"",
"},", "},",
"{", "{",
"\"system\": \"sec_scheme2\",", "\"system\": \"sec_scheme2\",",
"\"code\": \"sec_term2\",", "\"code\": \"sec_term2\",",
"\"display\": \"sec_label2\"", "\"display\": \"sec_label2\"",
"}", "}",
"],", "],",
"\"tag\": [", "\"tag\": [",
"{", "{",
"\"system\": \"scheme1\",", "\"system\": \"scheme1\",",
"\"code\": \"term1\",", "\"code\": \"term1\",",
"\"display\": \"label1\"", "\"display\": \"label1\"",
"},", "},",
"{", "{",
"\"system\": \"scheme2\",", "\"system\": \"scheme2\",",
"\"code\": \"term2\",", "\"code\": \"term2\",",
"\"display\": \"label2\"", "\"display\": \"label2\"",
"}", "}",
"]", "]",
"},")); "},"));
//@formatter:on //@formatter:on
Patient parsed = ourCtx.newJsonParser().parseResource(Patient.class, enc); Patient parsed = ourCtx.newJsonParser().parseResource(Patient.class, enc);
@ -506,6 +510,22 @@ public class JsonParserDstu3Test {
assertEquals("VERSION2", label.getVersion()); assertEquals("VERSION2", label.getVersion());
} }
@Test
public void testEncodeBinaryWithSecurityContext() {
Binary bin = new Binary();
bin.setContentType("text/plain");
bin.setContent("Now is the time".getBytes());
Reference securityContext = new Reference();
securityContext.setReference("DiagnosticReport/1");
bin.setSecurityContext(securityContext);
String encoded = ourCtx.newJsonParser().encodeResourceToString(bin);
ourLog.info(encoded);
assertThat(encoded, containsString("Binary"));
assertThat(encoded, containsString("\"contentType\":\"text/plain\""));
assertThat(encoded, containsString("\"content\":\"Tm93IGlzIHRoZSB0aW1l\""));
assertThat(encoded, containsString("\"securityContext\":{\"reference\":\"DiagnosticReport/1\"}"));
}
@Test @Test
public void testEncodeBundleNewBundleNoText() { public void testEncodeBundleNewBundleNoText() {
@ -539,22 +559,22 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
assertThat(encoded, stringContainsInOrder( assertThat(encoded, stringContainsInOrder(
"{", "{",
"\"resourceType\": \"Patient\",", "\"resourceType\": \"Patient\",",
"\"contained\": [", "\"contained\": [",
"{", "{",
"\"resourceType\": \"Condition\",", "\"resourceType\": \"Condition\",",
"\"id\": \"1\"", "\"id\": \"1\"",
"}", "}",
"],", "],",
"\"extension\": [", "\"extension\": [",
"{", "{",
"\"url\": \"test\",", "\"url\": \"test\",",
"\"valueReference\": {", "\"valueReference\": {",
"\"reference\": \"#1\"", "\"reference\": \"#1\"",
"}", "}",
"}", "}",
"],", "],",
"\"birthDate\": \"2016-04-05\"", "\"birthDate\": \"2016-04-05\"",
"}" "}"
)); ));
//@formatter:on //@formatter:on
@ -576,23 +596,6 @@ public class JsonParserDstu3Test {
assertEquals("{\"resourceType\":\"Binary\"}", output); assertEquals("{\"resourceType\":\"Binary\"}", output);
} }
@Test
public void testEncodeBinaryWithSecurityContext() {
Binary bin = new Binary();
bin.setContentType("text/plain");
bin.setContent("Now is the time".getBytes());
Reference securityContext = new Reference();
securityContext.setReference("DiagnosticReport/1");
bin.setSecurityContext(securityContext);
String encoded = ourCtx.newJsonParser().encodeResourceToString(bin);
assertThat(encoded, containsString("Binary"));
assertThat(encoded, containsString("contentType"));
assertThat(encoded, containsString("text/plain"));
assertThat(encoded, containsString("Tm93IGlzIHRoZSB0aW1l"));
assertThat(encoded, containsString("securityContext"));
assertThat(encoded, containsString("{\"reference\":\"DiagnosticReport/1\"}"));
}
/** /**
* #158 * #158
*/ */
@ -663,8 +666,8 @@ public class JsonParserDstu3Test {
ourLog.info(val); ourLog.info(val);
assertEquals( assertEquals(
"{\"resourceType\":\"Patient\",\"id\":\"123\",\"contact\":[{\"extension\":[{\"url\":\"http://foo.com/contact-eyecolour\",\"valueIdentifier\":{\"value\":\"EYE\"}}],\"name\":{\"family\":\"FAMILY\"}}]}", "{\"resourceType\":\"Patient\",\"id\":\"123\",\"contact\":[{\"extension\":[{\"url\":\"http://foo.com/contact-eyecolour\",\"valueIdentifier\":{\"value\":\"EYE\"}}],\"name\":{\"family\":\"FAMILY\"}}]}",
val); val);
FhirContext newCtx = FhirContext.forDstu3(); FhirContext newCtx = FhirContext.forDstu3();
PatientWithExtendedContactDstu3 actual = newCtx.newJsonParser().parseResource(PatientWithExtendedContactDstu3.class, val); PatientWithExtendedContactDstu3 actual = newCtx.newJsonParser().parseResource(PatientWithExtendedContactDstu3.class, val);
@ -706,9 +709,9 @@ public class JsonParserDstu3Test {
Patient p = new Patient(); Patient p = new Patient();
p.setId("Patient/B"); p.setId("Patient/B");
p p
.addExtension() .addExtension()
.setUrl("http://foo") .setUrl("http://foo")
.setValue(new Reference("Practitioner/A")); .setValue(new Reference("Practitioner/A"));
IParser parser = ourCtx.newJsonParser().setPrettyPrint(true); IParser parser = ourCtx.newJsonParser().setPrettyPrint(true);
parser.setDontEncodeElements(new HashSet<String>(Arrays.asList("*.id", "*.meta"))); parser.setDontEncodeElements(new HashSet<String>(Arrays.asList("*.id", "*.meta")));
@ -777,20 +780,20 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
assertThat(output, stringContainsInOrder( assertThat(output, stringContainsInOrder(
"\"id\": \"1\"", "\"id\": \"1\"",
"\"meta\"", "\"meta\"",
"\"extension\"", "\"extension\"",
"\"url\": \"http://exturl\"", "\"url\": \"http://exturl\"",
"\"extension\"", "\"extension\"",
"\"url\": \"http://subext\"", "\"url\": \"http://subext\"",
"\"valueString\": \"sub_ext_value\"", "\"valueString\": \"sub_ext_value\"",
"\"code\":" "\"code\":"
)); ));
assertThat(output, not(stringContainsInOrder( assertThat(output, not(stringContainsInOrder(
"\"url\": \"http://exturl\"", "\"url\": \"http://exturl\"",
",", ",",
"\"url\": \"http://exturl\"" "\"url\": \"http://exturl\""
))); )));
//@formatter:on //@formatter:on
obs = parser.parseResource(Observation.class, output); obs = parser.parseResource(Observation.class, output);
@ -823,14 +826,14 @@ public class JsonParserDstu3Test {
assertThat(encoded, stringContainsInOrder( assertThat(encoded, stringContainsInOrder(
"\"resourceType\": \"Patient\"", "\"resourceType\": \"Patient\"",
"\"contained\": [", "\"contained\": [",
"\"resourceType\": \"Condition\"", "\"resourceType\": \"Condition\"",
"\"id\": \"1\"", "\"id\": \"1\"",
"\"bodySite\": [", "\"bodySite\": [",
"\"text\": \"BODY SITE\"", "\"text\": \"BODY SITE\"",
"\"extension\": [", "\"extension\": [",
"\"url\": \"testCondition\",", "\"url\": \"testCondition\",",
"\"valueReference\": {", "\"valueReference\": {",
"\"reference\": \"#1\"", "\"reference\": \"#1\"",
"\"birthDate\": \"2016-04-14\"", "\"birthDate\": \"2016-04-14\"",
"}" "}"
)); ));
@ -1071,7 +1074,7 @@ public class JsonParserDstu3Test {
assertThat(encoded, containsString("Patient")); assertThat(encoded, containsString("Patient"));
assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"foo\",", "\"code\": \"bar\"", "\"system\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_SYSTEM + "\"", 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, not(containsString("THE DIV")));
assertThat(encoded, containsString("family")); assertThat(encoded, containsString("family"));
assertThat(encoded, not(containsString("maritalStatus"))); assertThat(encoded, not(containsString("maritalStatus")));
@ -1091,7 +1094,7 @@ public class JsonParserDstu3Test {
ourLog.info(enc); ourLog.info(enc);
assertEquals("{\"resourceType\":\"Patient\",\"meta\":{\"tag\":[{\"system\":\"scheme\",\"code\":\"term\",\"display\":\"display\"}]},\"identifier\":[{\"system\":\"sys\",\"value\":\"val\"}]}", assertEquals("{\"resourceType\":\"Patient\",\"meta\":{\"tag\":[{\"system\":\"scheme\",\"code\":\"term\",\"display\":\"display\"}]},\"identifier\":[{\"system\":\"sys\",\"value\":\"val\"}]}",
enc); enc);
} }
@ -1269,18 +1272,18 @@ public class JsonParserDstu3Test {
public void testExplanationOfBenefit() { public void testExplanationOfBenefit() {
//@formatter:off //@formatter:off
String input = "{" + String input = "{" +
" \"resourceType\": \"ExplanationOfBenefit\"," + " \"resourceType\": \"ExplanationOfBenefit\"," +
" \"insurance\": {\n" + " \"insurance\": {\n" +
" \"coverage\": {\n" + " \"coverage\": {\n" +
" \"reference\": \"Coverage/123\"\n" + " \"reference\": \"Coverage/123\"\n" +
" }\n" + " }\n" +
" },\n" + " },\n" +
" \"relationship\": {\n" + " \"relationship\": {\n" +
" \"system\": \"http://hl7.org/fhir/relationship\",\n" + " \"system\": \"http://hl7.org/fhir/relationship\",\n" +
" \"code\": \"1\",\n" + " \"code\": \"1\",\n" +
" \"display\": \"self\"\n" + " \"display\": \"self\"\n" +
" }\n" + " }\n" +
"}"; "}";
//@formatter:on //@formatter:on
ExplanationOfBenefit eob = ourCtx.newJsonParser().parseResource(ExplanationOfBenefit.class, input); 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 // ID should be a String and communication should be an Array
String input = "{\"resourceType\": \"Patient\",\n" + String input = "{\"resourceType\": \"Patient\",\n" +
" \"id\": 123,\n" + " \"id\": 123,\n" +
" \"communication\": {\n" + " \"communication\": {\n" +
" \"language\": {\n" + " \"language\": {\n" +
" \"text\": \"Hindi\"\n" + " \"text\": \"Hindi\"\n" +
" },\n" + " },\n" +
" \"preferred\": true\n" + " \"preferred\": true\n" +
" }\n" + " }\n" +
"}"; "}";
IParser p = ourCtx.newJsonParser(); IParser p = ourCtx.newJsonParser();
@ -1375,14 +1378,14 @@ public class JsonParserDstu3Test {
// ID should be a String and communication should be an Array // ID should be a String and communication should be an Array
String input = "{\"resourceType\": \"Patient\",\n" + String input = "{\"resourceType\": \"Patient\",\n" +
" \"id\": \"123\",\n" + " \"id\": \"123\",\n" +
" \"communication\": [{\n" + " \"communication\": [{\n" +
" \"language\": {\n" + " \"language\": {\n" +
" \"text\": \"Hindi\"\n" + " \"text\": \"Hindi\"\n" +
" },\n" + " },\n" +
" \"preferred\": true\n" + " \"preferred\": true\n" +
" }]\n" + " }]\n" +
"}"; "}";
IParser p = ourCtx.newJsonParser(); IParser p = ourCtx.newJsonParser();
@ -1504,13 +1507,13 @@ public class JsonParserDstu3Test {
public void testNamespacePreservationEncode() throws Exception { public void testNamespacePreservationEncode() throws Exception {
//@formatter:off //@formatter:off
String input = "<Patient xmlns=\"http://hl7.org/fhir\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">" + String input = "<Patient xmlns=\"http://hl7.org/fhir\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">" +
"<text>" + "<text>" +
"<xhtml:div>" + "<xhtml:div>" +
"<xhtml:img src=\"foo\"/>" + "<xhtml:img src=\"foo\"/>" +
"@fhirabend" + "@fhirabend" +
"</xhtml:div>" + "</xhtml:div>" +
"</text>" + "</text>" +
"</Patient>"; "</Patient>";
//@formatter:on //@formatter:on
Patient parsed = ourCtx.newXmlParser().parseResource(Patient.class, input); Patient parsed = ourCtx.newXmlParser().parseResource(Patient.class, input);
@ -1713,7 +1716,7 @@ public class JsonParserDstu3Test {
public void testParseAndEncodeBundleWithUuidBase() { public void testParseAndEncodeBundleWithUuidBase() {
//@formatter:off //@formatter:off
String input = String input =
"{\n" + "{\n" +
" \"resourceType\":\"Bundle\",\n" + " \"resourceType\":\"Bundle\",\n" +
" \"type\":\"document\",\n" + " \"type\":\"document\",\n" +
" \"entry\":[\n" + " \"entry\":[\n" +
@ -1795,33 +1798,33 @@ public class JsonParserDstu3Test {
public void testParseAndEncodeComments() { public void testParseAndEncodeComments() {
//@formatter:off //@formatter:off
String input = "{\n" + String input = "{\n" +
" \"resourceType\": \"Patient\",\n" + " \"resourceType\": \"Patient\",\n" +
" \"id\": \"pat1\",\n" + " \"id\": \"pat1\",\n" +
" \"text\": {\n" + " \"text\": {\n" +
" \"status\": \"generated\",\n" + " \"status\": \"generated\",\n" +
" \"div\": \"<div>\\n \\n <p>Patient Donald DUCK @ Acme Healthcare, Inc. MR = 654321</p>\\n \\n </div>\"\n" + " \"div\": \"<div>\\n \\n <p>Patient Donald DUCK @ Acme Healthcare, Inc. MR = 654321</p>\\n \\n </div>\"\n" +
" },\n" + " },\n" +
" \"identifier\": [\n" + " \"identifier\": [\n" +
" {\n" + " {\n" +
" \"fhir_comments\":[\"identifier comment 1\",\"identifier comment 2\"],\n" + " \"fhir_comments\":[\"identifier comment 1\",\"identifier comment 2\"],\n" +
" \"use\": \"usual\",\n" + " \"use\": \"usual\",\n" +
" \"_use\": {\n" + " \"_use\": {\n" +
" \"fhir_comments\":[\"use comment 1\",\"use comment 2\"]\n" + " \"fhir_comments\":[\"use comment 1\",\"use comment 2\"]\n" +
" },\n" + " },\n" +
" \"type\": {\n" + " \"type\": {\n" +
" \"coding\": [\n" + " \"coding\": [\n" +
" {\n" + " {\n" +
" \"system\": \"http://hl7.org/fhir/v2/0203\",\n" + " \"system\": \"http://hl7.org/fhir/v2/0203\",\n" +
" \"code\": \"MR\"\n" + " \"code\": \"MR\"\n" +
" }\n" + " }\n" +
" ]\n" + " ]\n" +
" },\n" + " },\n" +
" \"system\": \"urn:oid:0.1.2.3.4.5.6.7\",\n" + " \"system\": \"urn:oid:0.1.2.3.4.5.6.7\",\n" +
" \"value\": \"654321\"\n" + " \"value\": \"654321\"\n" +
" }\n" + " }\n" +
" ],\n" + " ],\n" +
" \"active\": true" + " \"active\": true" +
"}"; "}";
//@formatter:off //@formatter:off
Patient res = ourCtx.newJsonParser().parseResource(Patient.class, input); Patient res = ourCtx.newJsonParser().parseResource(Patient.class, input);
@ -1838,24 +1841,24 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
assertThat(encoded, stringContainsInOrder( assertThat(encoded, stringContainsInOrder(
"\"identifier\": [", "\"identifier\": [",
"{", "{",
"\"fhir_comments\":", "\"fhir_comments\":",
"[", "[",
"\"identifier comment 1\"", "\"identifier comment 1\"",
",", ",",
"\"identifier comment 2\"", "\"identifier comment 2\"",
"]", "]",
"\"use\": \"usual\",", "\"use\": \"usual\",",
"\"_use\": {", "\"_use\": {",
"\"fhir_comments\":", "\"fhir_comments\":",
"[", "[",
"\"use comment 1\"", "\"use comment 1\"",
",", ",",
"\"use comment 2\"", "\"use comment 2\"",
"]", "]",
"},", "},",
"\"type\"" "\"type\""
)); ));
//@formatter:off //@formatter:off
} }
@ -1865,7 +1868,7 @@ public class JsonParserDstu3Test {
Binary patient = new Binary(); Binary patient = new Binary();
patient.setId(new IdType("http://base/Binary/11/_history/22")); patient.setId(new IdType("http://base/Binary/11/_history/22"));
patient.setContentType("foo"); patient.setContentType("foo");
patient.setContent(new byte[] { 1, 2, 3, 4 }); patient.setContent(new byte[]{1, 2, 3, 4});
String val = ourCtx.newJsonParser().encodeResourceToString(patient); String val = ourCtx.newJsonParser().encodeResourceToString(patient);
@ -1916,7 +1919,7 @@ public class JsonParserDstu3Test {
"\"resourceType\":\"Patient\",", "\"resourceType\":\"Patient\",",
"\"id\":\"1\"", "\"id\":\"1\"",
"\"reference\":\"#1\"" "\"reference\":\"#1\""
)); ));
//@formatter:on //@formatter:on
o = parser.parseResource(Observation.class, enc); o = parser.parseResource(Observation.class, enc);
@ -1927,6 +1930,21 @@ public class JsonParserDstu3Test {
assertEquals("patient family", p.getName().get(0).getFamilyElement().getValue()); assertEquals("patient family", p.getName().get(0).getFamilyElement().getValue());
} }
/**
* See #720
*/
@Test
public void testParseCustomResourceType() {
String input = "{\"resourceType\":\"Bug720ResourceType\",\"meta\":{\"profile\":[\"http://example.com/StructureDefinition/dontuse#Bug720ResourceType\"]},\"supportedVersion\":\"2.5.x\",\"templatesConsentTemplate\":[{\"domainName\":\"name\",\"Name\":\"template_01\",\"version\":\"1.0\",\"title\":\"title\",\"comment\":\"comment\",\"contact\":{\"resourceType\":\"Person\",\"name\":[{\"family\":\"Mustermann\",\"given\":[\"Max\"]}],\"telecom\":[{\"system\":\"email\",\"value\":\"max.mustermann@mail.de\"},{\"system\":\"phone\",\"value\":\"+49 1234 23232\"}],\"address\":[{\"text\":\"street 1-2\",\"city\":\"city\",\"postalCode\":\"12345\",\"country\":\"Germany\"}]}}]}";
Bug720ResourceType parsed = ourCtx.newJsonParser().parseResource(Bug720ResourceType.class, input);
assertEquals(1, parsed.getTemplates().size());
assertEquals(Bug720Datatype.class, parsed.getTemplates().get(0).getClass());
assertEquals("Mustermann", ((Bug720Datatype) parsed.getTemplates().get(0)).getContact().getNameFirstRep().getFamily());
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(parsed));
}
/** /**
* #480 * #480
*/ */
@ -1989,8 +2007,8 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
String input = String input =
"{\"resourceType\":\"Patient\"," + "{\"resourceType\":\"Patient\"," +
"\"extension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + "\"extension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" +
"}"; "}";
//@formatter:on //@formatter:on
IParser parser = ourCtx.newJsonParser(); IParser parser = ourCtx.newJsonParser();
@ -2019,8 +2037,8 @@ public class JsonParserDstu3Test {
//@formatter:off //@formatter:off
String input = String input =
"{\"resourceType\":\"Patient\"," + "{\"resourceType\":\"Patient\"," +
"\"modifierExtension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" + "\"modifierExtension\":[ {\"valueDateTime\":\"2011-01-02T11:13:15\"} ]" +
"}"; "}";
//@formatter:on //@formatter:on
IParser parser = ourCtx.newJsonParser(); IParser parser = ourCtx.newJsonParser();
@ -2182,39 +2200,6 @@ public class JsonParserDstu3Test {
} }
} }
/**
* See #344
*/
@Test
public void testParserIsCaseSensitive() {
Observation obs = new Observation();
SampledData data = new SampledData();
data.setData("1 2 3");
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
data.setPeriod(1000L);
obs.setValue(data);
IParser p = ourCtx.newJsonParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
String encoded = p.encodeResourceToString(obs);
ourLog.info(encoded);
p.parseResource(encoded);
try {
p.parseResource(encoded.replace("Observation", "observation"));
fail();
} catch (DataFormatException e) {
assertEquals("Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'", e.getMessage());
}
try {
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
fail();
} catch (DataFormatException e) {
assertEquals("Unknown element 'valueSampleddata' found during parse", e.getMessage());
}
}
@Test @Test
public void testParseWithPrecision() { public void testParseWithPrecision() {
String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}"; String input = "{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.000000000000000100}}";
@ -2252,6 +2237,39 @@ public class JsonParserDstu3Test {
} }
} }
/**
* See #344
*/
@Test
public void testParserIsCaseSensitive() {
Observation obs = new Observation();
SampledData data = new SampledData();
data.setData("1 2 3");
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
data.setPeriod(1000L);
obs.setValue(data);
IParser p = ourCtx.newJsonParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
String encoded = p.encodeResourceToString(obs);
ourLog.info(encoded);
p.parseResource(encoded);
try {
p.parseResource(encoded.replace("Observation", "observation"));
fail();
} catch (DataFormatException e) {
assertEquals("Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'", e.getMessage());
}
try {
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
fail();
} catch (DataFormatException e) {
assertEquals("Unknown element 'valueSampleddata' found during parse", e.getMessage());
}
}
/** /**
* See #144 and #146 * See #144 and #146
*/ */
@ -2323,13 +2341,13 @@ public class JsonParserDstu3Test {
ArgumentCaptor<ScalarType> expectedScalar = ArgumentCaptor.forClass(ScalarType.class); ArgumentCaptor<ScalarType> expectedScalar = ArgumentCaptor.forClass(ScalarType.class);
ArgumentCaptor<ScalarType> actualScalar = ArgumentCaptor.forClass(ScalarType.class); ArgumentCaptor<ScalarType> actualScalar = ArgumentCaptor.forClass(ScalarType.class);
verify(errorHandler, atLeastOnce()).incorrectJsonType(Mockito.any(IParseLocation.class), elementName.capture(), expected.capture(), expectedScalar.capture(), actual.capture(), 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), 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), 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(), 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("_id", elementName.getAllValues().get(0));
assertEquals(ValueType.OBJECT, expected.getAllValues().get(0)); assertEquals(ValueType.OBJECT, expected.getAllValues().get(0));
@ -2361,26 +2379,6 @@ public class JsonParserDstu3Test {
assertTrue(result.isSuccessful()); assertTrue(result.isSuccessful());
} }
@Test
public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
String refVal = "http://my.org/FooBar";
Patient fhirPat = new Patient();
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
IParser parser = ourCtx.newJsonParser();
String output = parser.encodeResourceToString(fhirPat);
System.out.println("output: " + output);
// Deserialize then check that valueReference value is still correct
fhirPat = parser.parseResource(Patient.class, output);
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
Assert.assertEquals(1, extlst.size());
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
}
@AfterClass @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -82,6 +82,23 @@ public class XmlParserDstu3Test {
ourCtx.setNarrativeGenerator(null); ourCtx.setNarrativeGenerator(null);
} }
@Test
public void testEncodeBinaryWithSecurityContext() {
Binary bin = new Binary();
bin.setContentType("text/plain");
bin.setContent("Now is the time".getBytes());
Reference securityContext = new Reference();
securityContext.setReference("DiagnosticReport/1");
bin.setSecurityContext(securityContext);
String encoded = ourCtx.newXmlParser().encodeResourceToString(bin);
ourLog.info(encoded);
assertThat(encoded, containsString("Binary"));
assertThat(encoded, containsString("<contentType value=\"text/plain\"/>"));
assertThat(encoded, containsString("<securityContext><reference value=\"DiagnosticReport/1\"/></securityContext>"));
assertThat(encoded, containsString("<content value=\"Tm93IGlzIHRoZSB0aW1l\"/>"));
}
/** /**
* See #544 * See #544
*/ */

View File

@ -1,14 +1,16 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.junit.Assert.assertArrayEquals; import ca.uhn.fhir.context.FhirContext;
import static org.junit.Assert.assertEquals; import ca.uhn.fhir.rest.annotation.*;
import static org.junit.Assert.assertNull; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import java.util.concurrent.TimeUnit; import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
@ -18,18 +20,20 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Binary;
import org.junit.*; import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
public class CreateBinaryR4Test { import static org.junit.Assert.*;
public class BinaryServerR4Test {
private static CloseableHttpClient ourClient; private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4(); private static FhirContext ourCtx = FhirContext.forR4();
private static Binary ourLastBinary; private static Binary ourLastBinary;
@ -37,24 +41,73 @@ public class CreateBinaryR4Test {
private static String ourLastBinaryString; private static String ourLastBinaryString;
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
private static IdType ourLastId;
private static Binary ourNextBinary;
@Before @Before
public void before() { public void before() {
ourLastBinary = null; ourLastBinary = null;
ourLastBinaryBytes = null; ourLastBinaryBytes = null;
ourLastBinaryString = null; ourLastBinaryString = null;
ourLastId = null;
ourNextBinary = null;
} }
@Test @Test
public void testRawBytesBinaryContentType() throws Exception { public void testGetWithNoAccept() throws Exception {
ourNextBinary = new Binary();
ourNextBinary.setId("Binary/A/_history/222");
ourNextBinary.setContent(new byte[]{0, 1, 2, 3, 4});
ourNextBinary.setSecurityContext(new Reference("Patient/1"));
ourNextBinary.setContentType("application/foo");
HttpGet get = new HttpGet("http://localhost:" + ourPort + "/Binary/A");
get.addHeader("Content-Type", "application/foo");
CloseableHttpResponse status = ourClient.execute(get);
try {
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("application/foo", status.getEntity().getContentType().getValue());
assertEquals("Patient/1", status.getFirstHeader(Constants.HEADER_X_SECURITY_CONTEXT).getValue());
assertEquals("W/\"222\"", status.getFirstHeader(Constants.HEADER_ETAG).getValue());
assertEquals("http://localhost:" + ourPort + "/Binary/A/_history/222", status.getFirstHeader(Constants.HEADER_LOCATION).getValue());
byte[] content = IOUtils.toByteArray(status.getEntity().getContent());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, content);
} finally {
IOUtils.closeQuietly(status);
}
}
@Test
public void testPostBinaryWithSecurityContext() throws Exception {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary"); HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 })); post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
post.addHeader("Content-Type", "application/foo");
post.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, "Encounter/2");
CloseableHttpResponse status = ourClient.execute(post);
try {
assertNull(ourLastId);
assertEquals("application/foo", ourLastBinary.getContentType());
assertEquals("Encounter/2", ourLastBinary.getSecurityContext().getReference());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
} finally {
IOUtils.closeQuietly(status);
}
}
@Test
public void testPostRawBytesBinaryContentType() throws Exception {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
post.addHeader("Content-Type", "application/foo"); post.addHeader("Content-Type", "application/foo");
CloseableHttpResponse status = ourClient.execute(post); CloseableHttpResponse status = ourClient.execute(post);
try { try {
assertNull(ourLastId);
assertEquals("application/foo", ourLastBinary.getContentType()); assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent()); assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinaryBytes); assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
} finally { } finally {
IOUtils.closeQuietly(status); IOUtils.closeQuietly(status);
} }
@ -64,11 +117,11 @@ public class CreateBinaryR4Test {
* Technically the client shouldn't be doing it this way, but we'll be accepting * Technically the client shouldn't be doing it this way, but we'll be accepting
*/ */
@Test @Test
public void testRawBytesFhirContentType() throws Exception { public void testPostRawBytesFhirContentType() throws Exception {
Binary b = new Binary(); Binary b = new Binary();
b.setContentType("application/foo"); b.setContentType("application/foo");
b.setContent(new byte[] { 0, 1, 2, 3, 4 }); b.setContent(new byte[]{0, 1, 2, 3, 4});
String encoded = ourCtx.newJsonParser().encodeResourceToString(b); String encoded = ourCtx.newJsonParser().encodeResourceToString(b);
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary"); HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
@ -77,14 +130,14 @@ public class CreateBinaryR4Test {
CloseableHttpResponse status = ourClient.execute(post); CloseableHttpResponse status = ourClient.execute(post);
try { try {
assertEquals("application/foo", ourLastBinary.getContentType()); assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent()); assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
} finally { } finally {
IOUtils.closeQuietly(status); IOUtils.closeQuietly(status);
} }
} }
@Test @Test
public void testRawBytesFhirContentTypeContainingFhir() throws Exception { public void testPostRawBytesFhirContentTypeContainingFhir() throws Exception {
Patient p = new Patient(); Patient p = new Patient();
p.getText().setDivAsString("A PATIENT"); p.getText().setDivAsString("A PATIENT");
@ -109,13 +162,32 @@ public class CreateBinaryR4Test {
} }
@Test @Test
public void testRawBytesNoContentType() throws Exception { public void testPostRawBytesNoContentType() throws Exception {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary"); HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 })); post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
CloseableHttpResponse status = ourClient.execute(post); CloseableHttpResponse status = ourClient.execute(post);
try { try {
assertNull(ourLastBinary.getContentType()); assertNull(ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent()); assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
} finally {
IOUtils.closeQuietly(status);
}
}
@Test
public void testPutBinaryWithSecurityContext() throws Exception {
HttpPut post = new HttpPut("http://localhost:" + ourPort + "/Binary/A");
post.setEntity(new ByteArrayEntity(new byte[]{0, 1, 2, 3, 4}));
post.addHeader("Content-Type", "application/foo");
post.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, "Encounter/2");
CloseableHttpResponse status = ourClient.execute(post);
try {
assertEquals("Binary/A", ourLastId.getValue());
assertEquals("Binary/A", ourLastBinary.getId());
assertEquals("application/foo", ourLastBinary.getContentType());
assertEquals("Encounter/2", ourLastBinary.getSecurityContext().getReference());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinary.getContent());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourLastBinaryBytes);
} finally { } finally {
IOUtils.closeQuietly(status); IOUtils.closeQuietly(status);
} }
@ -149,7 +221,6 @@ public class CreateBinaryR4Test {
} }
public static class BinaryProvider implements IResourceProvider { public static class BinaryProvider implements IResourceProvider {
@Create() @Create()
public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) { public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
ourLastBinary = theBinary; ourLastBinary = theBinary;
@ -163,6 +234,20 @@ public class CreateBinaryR4Test {
return Binary.class; return Binary.class;
} }
@Read
public Binary read(@IdParam IdType theId) {
return ourNextBinary;
}
@Update()
public MethodOutcome updateBinary(@IdParam IdType theId, @ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
ourLastId = theId;
ourLastBinary = theBinary;
ourLastBinaryString = theBinaryString;
ourLastBinaryBytes = theBinaryBytes;
return new MethodOutcome(new IdType("Binary/001/_history/002"));
}
} }
} }

View File

@ -175,6 +175,11 @@
was not encoded correctly. Thanks to Malcolm McRoberts for the pull was not encoded correctly. Thanks to Malcolm McRoberts for the pull
request with fix and test case! request with fix and test case!
</action> </action>
<action type="add">
The Binary resource endpoint now supports the `X-Security-Context` header when
reading or writing Binary contents using their native Content-Type (i.e exchanging
the raw binary with the server, as opposed to exchanging a FHIR resource).
</action>
</release> </release>
<release version="3.0.0" date="2017-09-27"> <release version="3.0.0" date="2017-09-27">
<action type="add"> <action type="add">