diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java index a6d2730204e..c26f527c507 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java @@ -41,6 +41,7 @@ import ca.uhn.fhir.model.primitive.BoundCodeDt; import ca.uhn.fhir.model.primitive.BoundCodeableConceptDt; import ca.uhn.fhir.model.primitive.CodeDt; import ca.uhn.fhir.model.primitive.DateDt; +import ca.uhn.fhir.model.primitive.DecimalDt; import ca.uhn.fhir.model.primitive.ICodedDatatype; import ca.uhn.fhir.model.primitive.XhtmlDt; import ca.uhn.fhir.util.ReflectionUtil; @@ -125,7 +126,8 @@ class ModelScanner { private void init(Set> toScan) { toScan.add(DateDt.class); toScan.add(CodeDt.class); - + toScan.add(DecimalDt.class); + do { for (Class nextClass : toScan) { scan(nextClass); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseElement.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseElement.java index 615dc375316..d6179448e17 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseElement.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseElement.java @@ -26,6 +26,9 @@ public abstract class BaseElement implements IElement, ISupportsUndeclaredExtens @Override public IdDt getId() { + if (myId == null) { + myId = new IdDt(); + } return myId; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java new file mode 100644 index 00000000000..4dc2f5fee06 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.model.api; + +import java.util.List; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public interface IQueryParameterAnd { + + /** + * + * @see See FHIR specification + * 2.2.2 Search SearchParameter Types + * for information on the token format + */ + public void setValuesAsQueryTokens(List> theParameters) throws InvalidRequestException; + + /** + * + * @see See FHIR specification + * 2.2.2 Search SearchParameter Types + * for information on the token format + */ + public List> getValuesAsQueryTokens(); + + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/NarrativeDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/NarrativeDt.java index f58eab85710..5ecd1b429cc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/NarrativeDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/NarrativeDt.java @@ -137,8 +137,15 @@ public class NarrativeDt extends BaseElement implements ICompositeDatatype { myDiv = theValue; } + /** + * Sets the value using a textual DIV (or simple text block which will be + * converted to XHTML) + */ + public void setDiv(String theTextDiv) { + myDiv = new XhtmlDt(theTextDiv); + } + - } \ No newline at end of file diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/resource/DiagnosticReport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/resource/DiagnosticReport.java index 24bf9f71786..4af544c50a3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/resource/DiagnosticReport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/resource/DiagnosticReport.java @@ -296,10 +296,10 @@ public class DiagnosticReport extends BaseResource implements IResource { private DateTimeDt myIssued; @Child(name="subject", order=3, min=1, max=1, type={ - ca.uhn.fhir.model.dstu.resource.Patient.class, - ca.uhn.fhir.model.dstu.resource.Group.class, - ca.uhn.fhir.model.dstu.resource.Device.class, - ca.uhn.fhir.model.dstu.resource.Location.class, + Patient.class, + Group.class, + Device.class, + Location.class, }) @Description( shortDefinition="The subject of the report, usually, but not always, the patient", @@ -308,8 +308,8 @@ public class DiagnosticReport extends BaseResource implements IResource { private ResourceReferenceDt mySubject; @Child(name="performer", order=4, min=1, max=1, type={ - ca.uhn.fhir.model.dstu.resource.Practitioner.class, - ca.uhn.fhir.model.dstu.resource.Organization.class, + Practitioner.class, + Organization.class, }) @Description( shortDefinition="Responsible Diagnostic Service", @@ -325,7 +325,7 @@ public class DiagnosticReport extends BaseResource implements IResource { private IdentifierDt myIdentifier; @Child(name="requestDetail", order=6, min=0, max=Child.MAX_UNLIMITED, type={ - ca.uhn.fhir.model.dstu.resource.DiagnosticOrder.class, + DiagnosticOrder.class, }) @Description( shortDefinition="What was requested", @@ -351,7 +351,7 @@ public class DiagnosticReport extends BaseResource implements IResource { private IDatatype myDiagnostic; @Child(name="specimen", order=9, min=0, max=Child.MAX_UNLIMITED, type={ - ca.uhn.fhir.model.dstu.resource.Specimen.class, + Specimen.class, }) @Description( shortDefinition="Specimens this report is based on", @@ -360,7 +360,7 @@ public class DiagnosticReport extends BaseResource implements IResource { private java.util.List mySpecimen; @Child(name="result", order=10, min=0, max=Child.MAX_UNLIMITED, type={ - ca.uhn.fhir.model.dstu.resource.Observation.class, + Observation.class, }) @Description( shortDefinition="Observations - simple, or complex nested groups", @@ -369,7 +369,7 @@ public class DiagnosticReport extends BaseResource implements IResource { private java.util.List myResult; @Child(name="imagingStudy", order=11, min=0, max=Child.MAX_UNLIMITED, type={ - ca.uhn.fhir.model.dstu.resource.ImagingStudy.class, + ImagingStudy.class, }) @Description( shortDefinition="Reference to full details of imaging associated with the diagnostic report", @@ -450,7 +450,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myName = theValue; } - /** * Gets the value(s) for status (registered | partial | final | corrected +). @@ -481,7 +480,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myStatus = theValue; } - /** * Sets the value(s) for status (registered | partial | final | corrected +) * @@ -524,7 +522,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myIssued = theValue; } - /** * Sets the value for issued (Date this version was released) * @@ -533,8 +530,8 @@ public class DiagnosticReport extends BaseResource implements IResource { * The date and/or time that this version of the report was released from the source diagnostic service *

*/ - public void setIssued( Date theDate, TemporalPrecisionEnum thePrecision) { - myIssued = new DateTimeDt(theDate, thePrecision); + public void setIssuedWithSecondsPrecision( Date theDate) { + myIssued = new DateTimeDt(theDate); } /** @@ -545,8 +542,8 @@ public class DiagnosticReport extends BaseResource implements IResource { * The date and/or time that this version of the report was released from the source diagnostic service *

*/ - public void setIssuedWithSecondsPrecision( Date theDate) { - myIssued = new DateTimeDt(theDate); + public void setIssued( Date theDate, TemporalPrecisionEnum thePrecision) { + myIssued = new DateTimeDt(theDate, thePrecision); } @@ -576,7 +573,6 @@ public class DiagnosticReport extends BaseResource implements IResource { mySubject = theValue; } - /** * Gets the value(s) for performer (Responsible Diagnostic Service). @@ -604,7 +600,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myPerformer = theValue; } - /** * Gets the value(s) for identifier (Id for external references to this report). @@ -635,7 +630,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myIdentifier = theValue; } - /** * Gets the value(s) for requestDetail (What was requested). @@ -666,7 +660,19 @@ public class DiagnosticReport extends BaseResource implements IResource { myRequestDetail = theValue; } - + /** + * Adds and returns a new value for requestDetail (What was requested) + * + *

+ * Definition: + * Details concerning a test requested. + *

+ */ + public ResourceReferenceDt addRequestDetail() { + ResourceReferenceDt newType = new ResourceReferenceDt(); + getRequestDetail().add(newType); + return newType; + } /** * Gets the value(s) for serviceCategory (Biochemistry, Hematology etc.). @@ -697,7 +703,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myServiceCategory = theValue; } - /** * Gets the value(s) for diagnostic[x] (Physiologically Relevant time/time-period for report). @@ -725,7 +730,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myDiagnostic = theValue; } - /** * Gets the value(s) for specimen (Specimens this report is based on). @@ -756,7 +760,19 @@ public class DiagnosticReport extends BaseResource implements IResource { mySpecimen = theValue; } - + /** + * Adds and returns a new value for specimen (Specimens this report is based on) + * + *

+ * Definition: + * Details about the specimens on which this Disagnostic report is based + *

+ */ + public ResourceReferenceDt addSpecimen() { + ResourceReferenceDt newType = new ResourceReferenceDt(); + getSpecimen().add(newType); + return newType; + } /** * Gets the value(s) for result (Observations - simple, or complex nested groups). @@ -787,7 +803,19 @@ public class DiagnosticReport extends BaseResource implements IResource { myResult = theValue; } - + /** + * Adds and returns a new value for result (Observations - simple, or complex nested groups) + * + *

+ * Definition: + * Observations that are part of this diagnostic report. Observations can be simple name/value pairs (e.g. \"atomic\" results), or they can be grouping observations that include references to other members of the group (e.g. \"panels\") + *

+ */ + public ResourceReferenceDt addResult() { + ResourceReferenceDt newType = new ResourceReferenceDt(); + getResult().add(newType); + return newType; + } /** * Gets the value(s) for imagingStudy (Reference to full details of imaging associated with the diagnostic report). @@ -818,7 +846,19 @@ public class DiagnosticReport extends BaseResource implements IResource { myImagingStudy = theValue; } - + /** + * Adds and returns a new value for imagingStudy (Reference to full details of imaging associated with the diagnostic report) + * + *

+ * Definition: + * One or more links to full details of any imaging performed during the diagnostic investigation. Typically, this is imaging performed by DICOM enabled modalities, but this is not required. A fully enabled PACS viewer can use this information to provide views of the source images + *

+ */ + public ResourceReferenceDt addImagingStudy() { + ResourceReferenceDt newType = new ResourceReferenceDt(); + getImagingStudy().add(newType); + return newType; + } /** * Gets the value(s) for image (Key images associated with this report). @@ -862,7 +902,6 @@ public class DiagnosticReport extends BaseResource implements IResource { getImage().add(newType); return newType; } - /** * Gets the value(s) for conclusion (Clinical Interpretation of test results). @@ -893,7 +932,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myConclusion = theValue; } - /** * Sets the value for conclusion (Clinical Interpretation of test results) * @@ -949,7 +987,6 @@ public class DiagnosticReport extends BaseResource implements IResource { getCodedDiagnosis().add(newType); return newType; } - /** * Gets the value(s) for presentedForm (Entire Report as issued). @@ -993,7 +1030,6 @@ public class DiagnosticReport extends BaseResource implements IResource { getPresentedForm().add(newType); return newType; } - /** * Block class for child element: DiagnosticReport.image (Key images associated with this report) @@ -1014,7 +1050,7 @@ public class DiagnosticReport extends BaseResource implements IResource { private StringDt myComment; @Child(name="link", order=1, min=1, max=1, type={ - ca.uhn.fhir.model.dstu.resource.Media.class, + Media.class, }) @Description( shortDefinition="Reference to the image source", @@ -1067,7 +1103,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myComment = theValue; } - /** * Sets the value for comment (Comment about the image (e.g. explanation)) * @@ -1110,7 +1145,6 @@ public class DiagnosticReport extends BaseResource implements IResource { myLink = theValue; } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/DecimalDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/DecimalDt.java index b194525d800..ec1caabde51 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/DecimalDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/DecimalDt.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.model.primitive; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import ca.uhn.fhir.model.api.BasePrimitive; import ca.uhn.fhir.model.api.annotation.DatatypeDef; @@ -35,6 +37,31 @@ public class DecimalDt extends BasePrimitive { setValue(new BigDecimal(theValue)); } + /** + * Rounds the value to the given prevision + * + * @see MathContext#getPrecision() + */ + public void round(int thePrecision) { + if (getValue()!=null) { + BigDecimal newValue = getValue().round(new MathContext(thePrecision)); + setValue(newValue); + } + } + + /** + * Rounds the value to the given prevision + * + * @see MathContext#getPrecision() + * @see MathContext#getRoundingMode() + */ + public void round(int thePrecision, RoundingMode theRoundingMode) { + if (getValue()!=null) { + BigDecimal newValue = getValue().round(new MathContext(thePrecision, theRoundingMode)); + setValue(newValue); + } + } + /** * Constructor */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java index 27ccadb9ca2..08d1fa3c318 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java @@ -114,4 +114,16 @@ public class IdDt extends BasePrimitive { return myValue; } + /** + * Returns the value of this ID as a big decimal, or null if the value is null + * + * @throws NumberFormatException If the value is not a valid BigDecimal + */ + public BigDecimal asBigDecimal() { + if (getValue() == null){ + return null; + } + return new BigDecimal(getValueAsString()); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/XhtmlDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/XhtmlDt.java index 7f126d3f7cd..66b5ceada73 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/XhtmlDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/XhtmlDt.java @@ -66,17 +66,18 @@ public class XhtmlDt extends BasePrimitive> { XMLEventReader er = XMLInputFactory.newInstance().createXMLEventReader(new StringReader(val)); boolean first = true; while (er.hasNext()) { + XMLEvent next = er.nextEvent(); if (first) { first = false; continue; } - XMLEvent next = er.nextEvent(); if (er.hasNext()) { // don't add the last event value.add(next); } } - + setValue(value); + } catch (XMLStreamException e) { throw new DataFormatException("String does not appear to be valid XML/XHTML", e); } catch (FactoryConfigurationError e) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index 18381f3690a..01a7ce9947b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -43,6 +43,7 @@ import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; import ca.uhn.fhir.model.api.UndeclaredExtension; +import ca.uhn.fhir.model.dstu.composite.ContainedDt; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.StringDt; @@ -122,7 +123,7 @@ public class XmlParser extends BaseParser implements IParser { eventWriter.writeAttribute("type", "text/xml"); IResource resource = nextEntry.getResource(); - encodeResourceToXmlStreamWriter(resource, eventWriter); + encodeResourceToXmlStreamWriter(resource, eventWriter, false); eventWriter.writeEndElement(); // content eventWriter.writeEndElement(); // entry @@ -149,7 +150,7 @@ public class XmlParser extends BaseParser implements IParser { eventWriter = myXmlOutputFactory.createXMLStreamWriter(stringWriter); eventWriter = decorateStreamWriter(eventWriter); - encodeResourceToXmlStreamWriter(theResource, eventWriter); + encodeResourceToXmlStreamWriter(theResource, eventWriter, false); eventWriter.flush(); } catch (XMLStreamException e) { throw new ConfigurationException("Failed to initialize STaX event factory", e); @@ -323,6 +324,15 @@ public class XmlParser extends BaseParser implements IParser { } break; } + case CONTAINED_RESOURCES: { + ContainedDt value = (ContainedDt) nextValue; + theEventWriter.writeStartElement("contained"); + for (IResource next : value.getContainedResources()) { + encodeResourceToXmlStreamWriter(next, theEventWriter, true); + } + theEventWriter.writeEndElement(); + break; + } case RESOURCE: { throw new IllegalStateException(); // should not happen } @@ -333,8 +343,9 @@ public class XmlParser extends BaseParser implements IParser { } break; } + case EXTENSION_DECLARED: case UNDECL_EXT: { - throw new IllegalStateException("should not happen"); + throw new IllegalStateException("state should not happen: " + childDef.getName()); } } @@ -405,7 +416,7 @@ public class XmlParser extends BaseParser implements IParser { } - private void encodeResourceToXmlStreamWriter(IResource theResource, XMLStreamWriter eventWriter) throws XMLStreamException, DataFormatException { + private void encodeResourceToXmlStreamWriter(IResource theResource, XMLStreamWriter theEventWriter, boolean theIncludeResourceId) throws XMLStreamException, DataFormatException { super.containResourcesForEncoding(theResource); RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource); @@ -413,12 +424,16 @@ public class XmlParser extends BaseParser implements IParser { throw new ConfigurationException("Unknown resource type: " + theResource.getClass()); } - eventWriter.writeStartElement(resDef.getName()); - eventWriter.writeDefaultNamespace(FHIR_NS); + theEventWriter.writeStartElement(resDef.getName()); + theEventWriter.writeDefaultNamespace(FHIR_NS); - encodeCompositeElementToStreamWriter(theResource, eventWriter, resDef); + if (theIncludeResourceId && StringUtils.isNotBlank(theResource.getId().getValue())) { + theEventWriter.writeAttribute("id", theResource.getId().getValue()); + } + + encodeCompositeElementToStreamWriter(theResource, theEventWriter, resDef); - eventWriter.writeEndElement(); + theEventWriter.writeEndElement(); } private void encodeUndeclaredExtensions(XMLStreamWriter theWriter, List extensions, String tagName) throws XMLStreamException { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java index 200a06ae372..dd377575f76 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java @@ -1,12 +1,13 @@ package ca.uhn.fhir.rest.param; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.dstu.composite.CodingDt; -public class CodingListParam implements IQueryParameterOr { +public class CodingListParam implements IQueryParameterOr, Iterable { private List myCodings = new ArrayList(); @@ -14,6 +15,9 @@ public class CodingListParam implements IQueryParameterOr { * Returns all Codings associated with this list */ public List getCodings() { + if (myCodings == null) { + myCodings = new ArrayList(); + } return myCodings; } @@ -35,6 +39,7 @@ public class CodingListParam implements IQueryParameterOr { @Override public void setValuesAsQueryTokens(List theParameters) { + getCodings().clear(); for (String string : theParameters) { CodingDt dt = new CodingDt(); dt.setValueAsQueryToken(string); @@ -46,4 +51,9 @@ public class CodingListParam implements IQueryParameterOr { myCodings.add(theCodingDt); } + @Override + public Iterator iterator() { + return getCodings().iterator(); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java new file mode 100644 index 00000000000..b856ce05ba1 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java @@ -0,0 +1,103 @@ +package ca.uhn.fhir.rest.param; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class DateRangeParam implements IQueryParameterAnd { + + private QualifiedDateParam myLowerBound; + private QualifiedDateParam myUpperBound; + + private void addParam(QualifiedDateParam theParsed) throws InvalidRequestException { + if (theParsed.getComparator() == null) { + if (myLowerBound != null || myUpperBound != null) { + throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier"); + } + + myLowerBound = theParsed; + myUpperBound = theParsed; + // TODO: in this case, should set lower and upper to exact moments using specified precision + } else { + + switch (theParsed.getComparator()) { + case GREATERTHAN: + case GREATERTHAN_OR_EQUALS: + if (myLowerBound != null) { + throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound"); + } + myLowerBound = theParsed; + break; + case LESSTHAN: + case LESSTHAN_OR_EQUALS: + if (myUpperBound != null) { + throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound"); + } + myUpperBound = theParsed; + break; + default: + throw new InvalidRequestException("Unknown comparator: " + theParsed.getComparator()); + } + + } + } + + public QualifiedDateParam getLowerBound() { + return myLowerBound; + } + + public QualifiedDateParam getUpperBound() { + return myUpperBound; + } + + @Override + public List> getValuesAsQueryTokens() { + ArrayList> retVal = new ArrayList>(); + if (myLowerBound != null) { + retVal.add(Collections.singletonList(myLowerBound.getValueAsQueryToken())); + } + if (myUpperBound != null) { + retVal.add(Collections.singletonList(myUpperBound.getValueAsQueryToken())); + } + return retVal; + } + + public void setLowerBound(QualifiedDateParam theLowerBound) { + myLowerBound = theLowerBound; + } + + public void setUpperBound(QualifiedDateParam theUpperBound) { + myUpperBound = theUpperBound; + } + + @Override + public void setValuesAsQueryTokens(List> theParameters) throws InvalidRequestException { + for (List paramList : theParameters) { + if (paramList.size() == 0) { + continue; + } + if (paramList.size() > 1) { + throw new InvalidRequestException("DateRange parameter does not suppport OR queries"); + } + String param = paramList.get(0); + QualifiedDateParam parsed = new QualifiedDateParam(); + parsed.setValueAsQueryToken(param); + addParam(parsed); + } + } + + public Date getLowerBoundAsInstant() { + // TODO: account for precision + return myLowerBound.getValue(); + } + + public Date getUpperBoundAsInstant() { + // TODO: account for precision + return myUpperBound.getValue(); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QueryParameterAndBinder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QueryParameterAndBinder.java new file mode 100644 index 00000000000..aa973fd7cf0 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QueryParameterAndBinder.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.rest.param; + +import java.util.List; + +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +final class QueryParameterAndBinder implements IParamBinder { + private final Class myType; + + QueryParameterAndBinder(Class theType) { + myType = theType; + } + + @Override + public List> encode(Object theString) throws InternalErrorException { + List> retVal = ((IQueryParameterAnd) theString).getValuesAsQueryTokens(); + return retVal; + } + + @Override + public Object parse(List> theString) throws InternalErrorException, InvalidRequestException { + IQueryParameterAnd dt; + try { + dt = myType.newInstance(); + dt.setValuesAsQueryTokens(theString); + } catch (InstantiationException e) { + throw new InternalErrorException(e); + } catch (IllegalAccessException e) { + throw new InternalErrorException(e); + } catch (SecurityException e) { + throw new InternalErrorException(e); + } + return dt; + } +} \ No newline at end of file diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java index d8a36fd2dc1..7fe6ab3219b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.List; import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.dstu.composite.IdentifierDt; @@ -79,6 +80,8 @@ public class SearchParameter implements IParameter { this.parser = new QueryParameterTypeBinder((Class) type); } else if (IQueryParameterOr.class.isAssignableFrom(type)) { this.parser = new QueryParameterOrBinder((Class) type); + } else if (IQueryParameterAnd.class.isAssignableFrom(type)) { + this.parser = new QueryParameterAndBinder((Class) type); } else { throw new ConfigurationException("Unsupported data type for parameter: " + type.getCanonicalName()); } @@ -87,6 +90,8 @@ public class SearchParameter implements IParameter { myParamType = SearchParamTypeEnum.STRING; } else if (QualifiedDateParam.class.isAssignableFrom(type)) { myParamType = SearchParamTypeEnum.DATE; + } else if (DateRangeParam.class.isAssignableFrom(type)) { + myParamType = SearchParamTypeEnum.DATE; } else if (CodingListParam.class.isAssignableFrom(type)) { myParamType = SearchParamTypeEnum.TOKEN; } else if (IdentifierDt.class.isAssignableFrom(type)) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 88833a06627..7556775d070 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -186,6 +186,19 @@ public abstract class RestfulServer extends HttpServlet { String requestFullPath = StringUtils.defaultString(request.getRequestURI()); // String contextPath = StringUtils.defaultString(request.getContextPath()); String servletPath = StringUtils.defaultString(request.getServletPath()); + StringBuffer requestUrl = request.getRequestURL(); + String servletContextPath = ""; + if (request.getServletContext() != null) { + servletContextPath = StringUtils.defaultString(request.getServletContext().getContextPath()); + } + + ourLog.info("Request FullPath: {}", requestFullPath); + ourLog.info("Servlet Path: {}", servletPath); + ourLog.info("Request Url: {}", requestUrl); + ourLog.info("Context Path: {}", servletContextPath); + + servletPath = servletContextPath; + IdDt id = null; IdDt versionId = null; String operation = null; @@ -195,13 +208,10 @@ public abstract class RestfulServer extends HttpServlet { requestPath = requestPath.substring(1); } - StringBuffer requestUrl = request.getRequestURL(); int contextIndex = requestUrl.indexOf(servletPath); String fhirServerBase = requestUrl.substring(0, contextIndex + servletPath.length()); String completeUrl = StringUtils.isNotBlank(request.getQueryString()) ? requestUrl + "?" + request.getQueryString() : requestUrl.toString(); - ourLog.info("Request URI: {}", requestPath); - Map params = new HashMap(request.getParameterMap()); EncodingUtil responseEncoding = determineResponseEncoding(request, params); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java index c7ea30f8aa2..c508dc2d0d0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server.exceptions; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.primitive.IdDt; public class ResourceNotFoundException extends BaseServerResponseException { @@ -8,6 +10,10 @@ public class ResourceNotFoundException extends BaseServerResponseException { super(404, "Resource " + (theId != null ? theId.getValue() : "") + " is not known"); } + public ResourceNotFoundException(Class theClass, IdentifierDt thePatientId) { + super(404, "Resource of type " + theClass.getSimpleName() + " with ID " + thePatientId + " is not known"); + } + private static final long serialVersionUID = 1L; } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java index dbdc2cb259b..cc22506eacd 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java @@ -11,6 +11,8 @@ import net.sf.json.JSON; import net.sf.json.JSONSerializer; import org.apache.commons.io.IOUtils; +import org.hamcrest.core.IsNot; +import org.hamcrest.core.StringContains; import org.junit.Test; import ca.uhn.fhir.context.FhirContext; @@ -19,6 +21,8 @@ import ca.uhn.fhir.model.api.UndeclaredExtension; import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.resource.Specimen; +import ca.uhn.fhir.model.primitive.DecimalDt; public class JsonParserTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParserTest.class); @@ -88,5 +92,39 @@ public class JsonParserTest { } + @Test + public void testEncodeContainedResourcesMore() throws IOException { + + DiagnosticReport rpt = new DiagnosticReport(); + Specimen spm = new Specimen(); + spm.getText().setDiv("AAA"); + rpt.addSpecimen().setResource(spm); + + IParser p = new FhirContext(DiagnosticReport.class).newJsonParser().setPrettyPrint(true); + String str = p.encodeResourceToString(rpt); + + ourLog.info(str); + assertThat(str, StringContains.containsString("
AAA
")); + String substring = "\"resource\":\"#"; + assertThat(str, StringContains.containsString(substring)); + + int idx = str.indexOf(substring) + substring.length(); + int idx2 = str.indexOf('"', idx+1); + String id = str.substring(idx, idx2); + assertThat(str, StringContains.containsString("\"id\":\"" + id + "\"")); + assertThat(str, IsNot.not(StringContains.containsString(""))); + + } + + @Test + public void testEncodeInvalidChildGoodException() throws IOException { + Observation obs = new Observation(); + obs.setValue(new DecimalDt(112.22)); + + IParser p = new FhirContext(Observation.class).newXmlParser(); + + // Should have good error message + p.encodeResourceToString(obs); + } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java index 06801f1e0d9..3350b98d798 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java @@ -8,6 +8,8 @@ import java.io.StringReader; import org.apache.commons.io.IOUtils; import org.custommonkey.xmlunit.Diff; import org.custommonkey.xmlunit.XMLUnit; +import org.hamcrest.core.IsNot; +import org.hamcrest.core.StringContains; import org.junit.BeforeClass; import org.junit.Test; import org.xml.sax.SAXException; @@ -18,12 +20,15 @@ import ca.uhn.fhir.context.ResourceWithExtensionsA; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.composite.NarrativeDt; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.resource.Specimen; import ca.uhn.fhir.model.dstu.resource.ValueSet; import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum; +import ca.uhn.fhir.model.primitive.DecimalDt; public class XmlParserTest { @@ -54,6 +59,41 @@ public class XmlParserTest { } + @Test + public void testEncodeInvalidChildGoodException() throws IOException { + Observation obs = new Observation(); + obs.setValue(new DecimalDt(112.22)); + + IParser p = new FhirContext(Observation.class).newXmlParser(); + + // Should have good error message + p.encodeResourceToString(obs); + } + + @Test + public void testEncodeContainedResources() throws IOException { + + DiagnosticReport rpt = new DiagnosticReport(); + Specimen spm = new Specimen(); + spm.getText().setDiv("AAA"); + rpt.addSpecimen().setResource(spm); + + IParser p = new FhirContext(DiagnosticReport.class).newXmlParser().setPrettyPrint(true); + String str = p.encodeResourceToString(rpt); + + ourLog.info(str); + assertThat(str, StringContains.containsString("
AAA
")); + assertThat(str, StringContains.containsString("reference value=\"#")); + + int idx = str.indexOf("reference value=\"#") + "reference value=\"#".length(); + int idx2 = str.indexOf('"', idx+1); + String id = str.substring(idx, idx2); + assertThat(str, StringContains.containsString("")); + assertThat(str, IsNot.not(StringContains.containsString(""))); + + } + + @Test public void testParseBundle() { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java index 90106708d06..794e3744616 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java @@ -32,6 +32,7 @@ import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.param.CodingListParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; import ca.uhn.fhir.rest.server.Constants; @@ -134,6 +135,32 @@ public class ClientTest { } + + @Test + public void testSearchByDateRange() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = clientFactory.newClient(ITestClient.class, "http://foo"); + DateRangeParam param = new DateRangeParam(); + param.setLowerBound(new QualifiedDateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, "2011-01-01")); + param.setUpperBound(new QualifiedDateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, "2021-01-01")); + List response = client.getPatientByDateRange(param); + + assertEquals("http://foo/Patient?dateRange=%3E%3D2011-01-01&dateRange=%3C%3D2021-01-01", capt.getValue().getURI().toString()); + + } + + + + + @Test public void testSearchByDob() throws Exception { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java index 88c749fba84..9c7f6657dd3 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java @@ -15,6 +15,7 @@ import ca.uhn.fhir.rest.annotation.Required; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.client.api.IBasicClient; import ca.uhn.fhir.rest.param.CodingListParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; public interface ITestClient extends IBasicClient { @@ -34,6 +35,9 @@ public interface ITestClient extends IBasicClient { @Search() public List getPatientMultipleIdentifiers(@Required(name = "ids") CodingListParam theIdentifiers); + @Search() + public List getPatientByDateRange(@Required(name = "dateRange") DateRangeParam theIdentifiers); + @Search() public List getPatientByDob(@Required(name=Patient.SP_BIRTHDATE) QualifiedDateParam theBirthDate); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java index 194007fa214..56bfb189119 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java @@ -7,6 +7,7 @@ import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.experimental.theories.Theories; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; @@ -33,6 +35,7 @@ import ca.uhn.fhir.model.dstu.composite.CodingDt; import ca.uhn.fhir.model.dstu.composite.HumanNameDt; import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.dstu.resource.Conformance; +import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum; import ca.uhn.fhir.model.primitive.IdDt; @@ -45,6 +48,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Required; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.param.CodingListParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; import ca.uhn.fhir.rest.server.provider.ServerProfileProvider; import ca.uhn.fhir.testutil.RandomServerPortProvider; @@ -142,16 +146,16 @@ public class ResfulServerMethodTest { ourLog.info("Response:\n{}", enc); assertTrue(enc.contains(ExtensionConstants.CONF_ALSO_CHAIN)); } -// { -// IParser p = ourCtx.newJsonParser().setPrettyPrint(true); -// -// p.encodeResourceToWriter(bundle, new OutputStreamWriter(System.out)); -// -// String enc = p.encodeResourceToString(bundle); -// ourLog.info("Response:\n{}", enc); -// assertTrue(enc.contains(ExtensionConstants.CONF_ALSO_CHAIN)); -// -// } + // { + // IParser p = ourCtx.newJsonParser().setPrettyPrint(true); + // + // p.encodeResourceToWriter(bundle, new OutputStreamWriter(System.out)); + // + // String enc = p.encodeResourceToString(bundle); + // ourLog.info("Response:\n{}", enc); + // assertTrue(enc.contains(ExtensionConstants.CONF_ALSO_CHAIN)); + // + // } } @Test @@ -212,6 +216,18 @@ public class ResfulServerMethodTest { assertEquals("urn:bbb|bbb", patient.getIdentifier().get(2).getValueAsQueryToken()); } +// @Test +// public void testSearchByComplex() throws Exception { +// +// HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?Patient.identifier=urn:oid:2.16.840.1.113883.3.239.18.148%7C7000135&name=urn:oid:1.3.6.1.4.1.12201.102.5%7C522&date="); +// HttpResponse status = ourClient.execute(httpGet); +// +// String responseContent = IOUtils.toString(status.getEntity().getContent()); +// ourLog.info("Response was:\n{}", responseContent); +// +// assertEquals(200, status.getStatusLine().getStatusCode()); +// } + @Test public void testSearchByDob() throws Exception { @@ -361,11 +377,30 @@ public class ResfulServerMethodTest { assertEquals(200, status.getStatusLine().getStatusCode()); - // TODO: enable once JSON parser is written - // Patient patient = (Patient) - // ourCtx.newJsonParser().parseResource(responseContent); - // assertEquals("PatientOne", - // patient.getName().get(0).getGiven().get(0).getValue()); + Patient patient = (Patient) ourCtx.newJsonParser().parseResource(responseContent); + assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue()); + + } + + @Test + public void testDateRangeParam() throws Exception { + + // HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + + // "/Patient/1"); + // httpPost.setEntity(new StringEntity("test", + // ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?dateRange=%3E%3D2011-01-01&dateRange=%3C%3D2021-01-01"); + HttpResponse status = ourClient.execute(httpGet); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + + Patient patient = (Patient) ourCtx.newXmlParser().parseBundle(responseContent).getEntries().get(0).getResource(); + assertEquals(">=2011-01-01", patient.getName().get(0).getSuffix().get(0).getValue()); + assertEquals("<=2021-01-01", patient.getName().get(0).getSuffix().get(1).getValue()); } @@ -517,6 +552,15 @@ public class ResfulServerMethodTest { return null; } + @SuppressWarnings("unused") + public List findDiagnosticReportsByPatient( + @Required(name="Patient.identifier") IdentifierDt thePatientId, + @Required(name=DiagnosticReport.SP_NAME) CodingListParam theNames, + @Optional(name=DiagnosticReport.SP_DATE) DateRangeParam theDateRange + ) throws Exception { + return Collections.emptyList(); + } + @Search() public Patient getPatientWithDOB(@Required(name = "dob") QualifiedDateParam theDob) { Patient next = getIdToPatient().get("1"); @@ -601,6 +645,14 @@ public class ResfulServerMethodTest { return Patient.class; } + @Search() + public Patient getPatientByDateRange(@Required(name = "dateRange") DateRangeParam theIdentifiers) { + Patient retVal = getIdToPatient().get("1"); + retVal.getName().get(0).addSuffix().setValue(theIdentifiers.getLowerBound().getValueAsQueryToken()); + retVal.getName().get(0).addSuffix().setValue(theIdentifiers.getUpperBound().getValueAsQueryToken()); + return retVal; + } + } } diff --git a/hapi-tinder-plugin/src/main/resources/vm/templates.vm b/hapi-tinder-plugin/src/main/resources/vm/templates.vm index 25771f57864..059e182da7c 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/templates.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/templates.vm @@ -106,7 +106,21 @@ return newType; } #end - +#if( ${child.repeatable} && ${child.singleChildInstantiable} && ${child.resourceRef} ) + /** + * Adds and returns a new value for ${child.elementName} (${child.shortName}) + * + *

+ * Definition: + * ${child.definition} + *

+ */ + public ResourceReferenceDt add${child.methodName}() { + ResourceReferenceDt newType = new ResourceReferenceDt(); + get${child.methodName}().add(newType); + return newType; + } +#end #if ( ${child.boundCode} && ${child.repeatable} ) /** * Add a value for ${child.elementName} (${child.shortName})