diff --git a/examples/pom.xml.orig.orig b/examples/pom.xml.orig.orig deleted file mode 100644 index b8f9f336f12..00000000000 --- a/examples/pom.xml.orig.orig +++ /dev/null @@ -1,61 +0,0 @@ - - 4.0.0 - - - ca.uhn.hapi.fhir - hapi-fhir - 0.8-SNAPSHOT - ../../pom.xml - - - hapi-fhir-base-examples - jar - - HAPI FHIR - Examples (for site) - - - - ca.uhn.hapi.fhir - hapi-fhir-base - 0.8-SNAPSHOT -<<<<<<< HEAD:hapi-fhir-base/examples/pom.xml -======= -<<<<<<< HEAD -======= ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2:examples/pom.xml.orig - - - ca.uhn.hapi.fhir - hapi-fhir-structures-dstu - 0.8-SNAPSHOT -<<<<<<< HEAD:hapi-fhir-base/examples/pom.xml -======= ->>>>>>> versions ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2:examples/pom.xml.orig - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - junit - junit - ${junit_version} - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - - diff --git a/examples/src/main/java/example/GenericClientExample.java b/examples/src/main/java/example/GenericClientExample.java index 563a2a2a09a..e891d6da18a 100644 --- a/examples/src/main/java/example/GenericClientExample.java +++ b/examples/src/main/java/example/GenericClientExample.java @@ -11,9 +11,12 @@ import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.valueset.AdministrativeGenderCodesEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.method.SearchStyleEnum; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; public class GenericClientExample { @@ -48,12 +51,19 @@ public class GenericClientExample { // Invoke the server create method (and send pretty-printed JSON // encoding to the server // instead of the default which is non-pretty printed XML) - client - .create() + MethodOutcome outcome = client.create() .resource(patient) .prettyPrint() .encodedJson() .execute(); + + // The MethodOutcome object will contain information about the + // response from the server, including the ID of the created + // resource, the OperationOutcome response, etc. (assuming that + // any of these things were provided by the server! They may not + // always be) + IdDt id = outcome.getId(); + System.out.println("Got ID: " + id.getValue()); // END SNIPPET: create } { @@ -69,15 +79,47 @@ public class GenericClientExample { // have one though) patient.setId("Patient/123"); - // Invoke the server create method (and send pretty-printed JSON - // encoding to the server - // instead of the default which is non-pretty printed XML) - client - .update() + // Invoke the server update method + MethodOutcome outcome = client.update() .resource(patient) .execute(); + + // The MethodOutcome object will contain information about the + // response from the server, including the ID of the created + // resource, the OperationOutcome response, etc. (assuming that + // any of these things were provided by the server! They may not + // always be) + IdDt id = outcome.getId(); + System.out.println("Got ID: " + id.getValue()); // END SNIPPET: update } + { + // START SNIPPET: etagupdate + // First, let's retrive the latest version of a resource + // from the server + Patient patient = client.read().resource(Patient.class).withId("123").execute(); + + // If the server is a version aware server, we should now know the latest version + // of the resource + System.out.println("Version ID: " + patient.getId().getVersionIdPart()); + + // Now let's make a change to the resource + patient.setGender(AdministrativeGenderCodesEnum.F); + + // Invoke the server update method - Because the resource has + // a version, it will be included in the request sent to + // the server + try { + MethodOutcome outcome = client + .update() + .resource(patient) + .execute(); + } catch (PreconditionFailedException e) { + // If we get here, the latest version has changed + // on the server so our update failed. + } + // END SNIPPET: etagupdate + } { // START SNIPPET: conformance // Retrieve the server's conformance statement and print its @@ -180,23 +222,46 @@ public class GenericClientExample { { // START SNIPPET: read - IdDt id = new IdDt("Patient", "123"); - Patient patient = client.read(Patient.class, id); // search for patient 123 + // search for patient 123 + Patient patient = client.read() + .resource(Patient.class) + .withId("123") + .execute(); // END SNIPPET: read } { // START SNIPPET: vread - IdDt id = new IdDt("Patient", "123", "888"); - Patient patient = client.vread(Patient.class, id); // search for version 888 of patient 123 + // search for patient 123 (specific version 888) + Patient patient = client.read() + .resource(Patient.class) + .withIdAndVersion("123", "888") + .execute(); // END SNIPPET: vread } { // START SNIPPET: readabsolute - IdDt id = new IdDt("http://example.com/fhir/Patient/123"); - Patient patient = client.read(Patient.class, id); // search for patient 123 on example.com + // search for patient 123 on example.com + String url = "http://example.com/fhir/Patient/123"; + Patient patient = client.read() + .resource(Patient.class) + .withUrl(url) + .execute(); // END SNIPPET: readabsolute } + { + // START SNIPPET: etagread + // search for patient 123 + Patient patient = client.read() + .resource(Patient.class) + .withId("123") + .ifVersionMatches("001").returnNull() + .execute(); + if (patient == null) { + // resource has not changed + } + // END SNIPPET: etagread + } diff --git a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java index f454c958fbf..3b1f81482e3 100644 --- a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java +++ b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java @@ -72,6 +72,7 @@ import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -661,11 +662,13 @@ public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient t String versionId = theId.getVersionIdPart(); if (versionId != null) { - // If the client passed in a version number in the request URL, which means they are + // If the client passed in a version number in an If-Match header, they are // doing a version-aware update. You may wish to throw an exception if the supplied - // version is not the latest version. + // version is not the latest version. Note that as of DSTU2 the FHIR specification uses + // ETags and If-Match to handle version aware updates, so PreconditionFailedException (HTTP 412) + // is used instead of ResourceVersionConflictException (HTTP 409) if (detectedVersionConflict) { - throw new ResourceVersionConflictException("Invalid version"); + throw new PreconditionFailedException("Unexpected version"); } } diff --git a/examples/src/main/java/example/ServerETagExamples.java b/examples/src/main/java/example/ServerETagExamples.java new file mode 100644 index 00000000000..b85f42c39ad --- /dev/null +++ b/examples/src/main/java/example/ServerETagExamples.java @@ -0,0 +1,29 @@ +package example; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; + +import ca.uhn.fhir.rest.server.ETagSupportEnum; +import ca.uhn.fhir.rest.server.RestfulServer; + +@SuppressWarnings("serial") +public class ServerETagExamples { + + // START SNIPPET: disablingETags + @WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server") + public class RestfulServerWithLogging extends RestfulServer { + + @Override + protected void initialize() throws ServletException { + // ... define your resource providers here ... + + // ETag support is enabled by default + setETagSupport(ETagSupportEnum.ENABLED); + } + + } + // END SNIPPET: disablingETags + + + +} diff --git a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs index 42246e57dab..1c96a5fe526 100644 --- a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs +++ b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs @@ -25,9 +25,9 @@ org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning @@ -36,12 +36,12 @@ org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning @@ -79,7 +79,7 @@ org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled @@ -94,7 +94,7 @@ org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference= org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java.orig deleted file mode 100644 index 199bfc5fa40..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java.orig +++ /dev/null @@ -1,409 +0,0 @@ -package ca.uhn.fhir.model.primitive; - -/* - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.*; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.TimeZone; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.time.DateUtils; -import org.apache.commons.lang3.time.FastDateFormat; - -import ca.uhn.fhir.model.api.BasePrimitive; -import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.parser.DataFormatException; - -public abstract class BaseDateTimeDt extends BasePrimitive { - - /* - * Add any new formatters to the static block below!! - */ - private static final List ourFormatters; - private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}"); - private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}"); - private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy"); - private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd"); - private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd"); - private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}"); - private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss"); - private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS"); - private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC")); - private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); - private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")); - private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ"); - private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM"); - private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM"); - private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}"); - private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}"); - - static { - ArrayList formatters = new ArrayList(); - formatters.add(ourYearFormat); - formatters.add(ourYearMonthDayFormat); - formatters.add(ourYearMonthDayNoDashesFormat); - formatters.add(ourYearMonthDayTimeFormat); - formatters.add(ourYearMonthDayTimeMilliFormat); - formatters.add(ourYearMonthDayTimeUTCZFormat); - formatters.add(ourYearMonthDayTimeMilliUTCZFormat); - formatters.add(ourYearMonthDayTimeMilliZoneFormat); - formatters.add(ourYearMonthDayTimeZoneFormat); - formatters.add(ourYearMonthFormat); - formatters.add(ourYearMonthNoDashesFormat); - ourFormatters = Collections.unmodifiableList(formatters); - } - private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND; - - private TimeZone myTimeZone; - private boolean myTimeZoneZulu = false; - - private void clearTimeZone() { - myTimeZone = null; - myTimeZoneZulu = false; -<<<<<<< HEAD - } - - /** - * Gets the precision for this datatype using field values from {@link Calendar}, such as {@link Calendar#MONTH}. Default is {@link Calendar#DAY_OF_MONTH} - * - * @see #setPrecision(int) - */ - public TemporalPrecisionEnum getPrecision() { - return myPrecision; - } - - public TimeZone getTimeZone() { - return myTimeZone; - } - - @Override - public Date getValue() { - return myValue; -======= ->>>>>>> issue50 - } - - @Override - protected String encode(Date theValue) { - if (theValue == null) { - return null; - } else { - switch (myPrecision) { - case DAY: - return ourYearMonthDayFormat.format(theValue); - case MONTH: - return ourYearMonthFormat.format(theValue); - case YEAR: - return ourYearFormat.format(theValue); - case SECOND: - if (myTimeZoneZulu) { - GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - cal.setTime(theValue); - return ourYearMonthDayTimeFormat.format(cal) + "Z"; - } else if (myTimeZone != null) { - GregorianCalendar cal = new GregorianCalendar(myTimeZone); - cal.setTime(theValue); - return ourYearMonthDayTimeZoneFormat.format(cal); - } else { - return ourYearMonthDayTimeFormat.format(theValue); - } - case MILLI: - if (myTimeZoneZulu) { - GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - cal.setTime(theValue); - return ourYearMonthDayTimeMilliFormat.format(cal) + "Z"; - } else if (myTimeZone != null) { - GregorianCalendar cal = new GregorianCalendar(myTimeZone); - cal.setTime(theValue); - return ourYearMonthDayTimeMilliZoneFormat.format(cal); - } else { - return ourYearMonthDayTimeMilliFormat.format(theValue); - } - } - throw new IllegalStateException("Invalid precision (this is a HAPI bug, shouldn't happen): " + myPrecision); - } - } - - /** -<<<<<<< HEAD -======= - * Gets the precision for this datatype using field values from {@link Calendar}, such as {@link Calendar#MONTH}. Default is {@link Calendar#DAY_OF_MONTH} - * - * @see #setPrecision(int) - */ - public TemporalPrecisionEnum getPrecision() { - return myPrecision; - } - - public TimeZone getTimeZone() { - return myTimeZone; - } - - /** ->>>>>>> issue50 - * To be implemented by subclasses to indicate whether the given precision is allowed by this type - */ - abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision); - - public boolean isTimeZoneZulu() { - return myTimeZoneZulu; - } - - /** - * Returns true if this object represents a date that is today's date - * - * @throws NullPointerException - * if {@link #getValue()} returns null - */ - public boolean isToday() { -<<<<<<< HEAD - Validate.notNull(myValue, getClass().getSimpleName() + " contains null value"); - return DateUtils.isSameDay(new Date(), myValue); - } - - /** - * Sets the precision for this datatype using field values from {@link Calendar}. Valid values are: - *
    - *
  • {@link Calendar#SECOND} - *
  • {@link Calendar#DAY_OF_MONTH} - *
  • {@link Calendar#MONTH} - *
  • {@link Calendar#YEAR} - *
- * - * @throws DataFormatException - */ - public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException { - if (thePrecision == null) { - throw new NullPointerException("Precision may not be null"); - } - myPrecision = thePrecision; - } - - private void setTimeZone(String theValueString, boolean hasMillis) { - clearTimeZone(); - int timeZoneStart = 19; - if (hasMillis) - timeZoneStart += 4; - if (theValueString.endsWith("Z")) { - setTimeZoneZulu(true); - } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) { - setTimeZone(TimeZone.getTimeZone(theValueString.substring(timeZoneStart))); - } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) { - setTimeZone(TimeZone.getTimeZone("GMT" + theValueString.substring(timeZoneStart))); - } - } - - public void setTimeZone(TimeZone theTimeZone) { - myTimeZone = theTimeZone; - } - - public void setTimeZoneZulu(boolean theTimeZoneZulu) { - myTimeZoneZulu = theTimeZoneZulu; -======= - Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value"); - return DateUtils.isSameDay(new Date(), getValue()); ->>>>>>> issue50 - } - - @Override - protected Date parse(String theValue) throws DataFormatException { - try { - if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) { - if (isPrecisionAllowed(YEAR)) { - setPrecision(YEAR); - clearTimeZone(); - return ((ourYearFormat).parse(theValue)); - } else { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support YEAR precision): " + theValue); - } - } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) { - // Eg. 198401 (allow this just to be lenient) - if (isPrecisionAllowed(MONTH)) { - setPrecision(MONTH); - clearTimeZone(); - return ((ourYearMonthNoDashesFormat).parse(theValue)); - } else { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); - } - } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) { - // E.g. 1984-01 (this is valid according to the spec) - if (isPrecisionAllowed(MONTH)) { - setPrecision(MONTH); - clearTimeZone(); - return ((ourYearMonthFormat).parse(theValue)); - } else { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support MONTH precision): " + theValue); - } - } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) { - // Eg. 19840101 (allow this just to be lenient) - if (isPrecisionAllowed(DAY)) { - setPrecision(DAY); - clearTimeZone(); - return ((ourYearMonthDayNoDashesFormat).parse(theValue)); - } else { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); - } - } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) { - // E.g. 1984-01-01 (this is valid according to the spec) - if (isPrecisionAllowed(DAY)) { - setPrecision(DAY); - clearTimeZone(); - return ((ourYearMonthDayFormat).parse(theValue)); - } else { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); - } - } else if (theValue.length() >= 18) { // date and time with possible time zone - int dotIndex = theValue.indexOf('.', 18); - boolean hasMillis = dotIndex > -1; - - if (!hasMillis && !isPrecisionAllowed(SECOND)) { - throw new DataFormatException("Invalid date/time string (data type does not support SECONDS precision): " + theValue); - } else if (hasMillis && !isPrecisionAllowed(MILLI)) { - throw new DataFormatException("Invalid date/time string (data type " + getClass().getSimpleName() + " does not support MILLIS precision):" + theValue); - } - -<<<<<<< HEAD - if (hasMillis) { - try { - if (hasOffset(theValue)) { - myValue = ourYearMonthDayTimeMilliZoneFormat.parse(theValue); - } else if (theValue.endsWith("Z")) - myValue = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue); - else - myValue = ourYearMonthDayTimeMilliFormat.parse(theValue); - } catch (ParseException p2) { - throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); - } - setTimeZone(theValue, hasMillis); -======= - Calendar cal; - try { - cal = DatatypeConverter.parseDateTime(theValue); - } catch (IllegalArgumentException e) { - throw new DataFormatException("Invalid data/time string (" + e.getMessage() + "): " + theValue); - } - if (dotIndex == -1) { - setPrecision(TemporalPrecisionEnum.SECOND); - } else { ->>>>>>> issue50 - setPrecision(TemporalPrecisionEnum.MILLI); - } else { - try { - if (hasOffset(theValue)) { - myValue = ourYearMonthDayTimeZoneFormat.parse(theValue); - } else if (theValue.endsWith("Z")) { - myValue = ourYearMonthDayTimeUTCZFormat.parse(theValue); - } else { - myValue = ourYearMonthDayTimeFormat.parse(theValue); - } - } catch (ParseException p2) { - throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); - } - - setTimeZone(theValue, hasMillis); - setPrecision(TemporalPrecisionEnum.SECOND); - } -<<<<<<< HEAD -======= - - return cal.getTime(); ->>>>>>> issue50 - } else { - throw new DataFormatException("Invalid date/time string (invalid length): " + theValue); - } - } catch (ParseException e) { - throw new DataFormatException("Invalid date string (" + e.getMessage() + "): " + theValue); - } - } - -<<<<<<< HEAD - private boolean hasOffset(String theValue) { - boolean inTime = false; - for (int i = 0; i < theValue.length(); i++) { - switch (theValue.charAt(i)) { - case 'T': - inTime = true; - break; - case '+': - case '-': - if (inTime) { - return true; - } - break; - } - } - return false; - } - - /** - * For unit tests only - */ - static List getFormatters() { - return ourFormatters; -======= - /** - * Sets the precision for this datatype using field values from {@link Calendar}. Valid values are: - *
    - *
  • {@link Calendar#SECOND} - *
  • {@link Calendar#DAY_OF_MONTH} - *
  • {@link Calendar#MONTH} - *
  • {@link Calendar#YEAR} - *
- * - * @throws DataFormatException - */ - public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException { - if (thePrecision == null) { - throw new NullPointerException("Precision may not be null"); - } - myPrecision = thePrecision; - } - - public void setTimeZone(TimeZone theTimeZone) { - myTimeZone = theTimeZone; - } - - public void setTimeZoneZulu(boolean theTimeZoneZulu) { - myTimeZoneZulu = theTimeZoneZulu; - } - - @Override - public void setValue(Date theValue) throws DataFormatException { - clearTimeZone(); - super.setValue(theValue); - } - - @Override - public void setValueAsString(String theValue) throws DataFormatException { - clearTimeZone(); - super.setValueAsString(theValue); ->>>>>>> issue50 - } - -} 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 c0e25cd1c3a..5b4e6d4332e 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 @@ -451,6 +451,11 @@ public class IdDt implements IPrimitiveDatatype { return getValue(); } + /** + * Returns a new IdDt containing this IdDt's values but with no server base URL if one + * is present in this IdDt. For example, if this IdDt contains the ID "http://foo/Patient/1", + * this method will return a new IdDt containing ID "Patient/1". + */ public IdDt toUnqualified() { return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); } @@ -550,5 +555,21 @@ public class IdDt implements IPrimitiveDatatype { throw new IllegalArgumentException("Unknown resource class type, does not implement IResource or extend Resource"); } } - + + /** + * Retrieves the ID from the given resource instance + */ + public static IdDt of(IBaseResource theResouce) { + if (theResouce == null) { + throw new NullPointerException("theResource can not be null"); + } else if (theResouce instanceof IResource) { + return ((IResource) theResouce).getId(); + } else if (theResouce instanceof Resource) { + // TODO: implement + throw new UnsupportedOperationException(); + } else { + throw new IllegalArgumentException("Unknown resource class type, does not implement IResource or extend Resource"); + } + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java.orig deleted file mode 100644 index 9084e3999fd..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java.orig +++ /dev/null @@ -1,556 +0,0 @@ -package ca.uhn.fhir.model.primitive; - -/* - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import static org.apache.commons.lang3.StringUtils.*; - -import java.math.BigDecimal; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.builder.HashCodeBuilder; -<<<<<<< HEAD -import org.hl7.fhir.instance.model.IBaseResource; -import org.hl7.fhir.instance.model.Resource; -======= -import org.hamcrest.core.IsNot; ->>>>>>> c294e1c064fcbf112edcbf4e10c341691c12a1a8 - -import ca.uhn.fhir.model.api.IPrimitiveDatatype; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.api.annotation.DatatypeDef; -import ca.uhn.fhir.model.api.annotation.SimpleSetter; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.util.UrlUtil; - -/** - * Represents the FHIR ID type. This is the actual resource ID, meaning the ID that will be used in RESTful URLs, Resource References, etc. to represent a specific instance of a resource. - * - *

- * Description: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length - * limit of 36 characters. - *

- *

- * regex: [a-z0-9\-\.]{1,36} - *

- */ -@DatatypeDef(name = "id") -public class IdDt implements IPrimitiveDatatype { - - private String myBaseUrl; - private boolean myHaveComponentParts; - private String myResourceType; - private String myUnqualifiedId; - private String myUnqualifiedVersionId; - private volatile String myValue; - - /** - * Create a new empty ID - */ - public IdDt() { - super(); - } - - /** - * Create a new ID, using a BigDecimal input. Uses {@link BigDecimal#toPlainString()} to generate the string representation. - */ - public IdDt(BigDecimal thePid) { - if (thePid != null) { - setValue(toPlainStringWithNpeThrowIfNeeded(thePid)); - } else { - setValue(null); - } - } - - /** - * Create a new ID using a long - */ - public IdDt(long theId) { - setValue(Long.toString(theId)); - } - - /** - * Create a new ID using a string. This String may contain a simple ID (e.g. "1234") or it may contain a complete URL (http://example.com/fhir/Patient/1234). - * - *

- * Description: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length - * limit of 36 characters. - *

- *

- * regex: [a-z0-9\-\.]{1,36} - *

- */ - @SimpleSetter - public IdDt(@SimpleSetter.Parameter(name = "theId") String theValue) { - setValue(theValue); - } - - /** - * Constructor - * - * @param theResourceType - * The resource type (e.g. "Patient") - * @param theId - * The ID (e.g. "123") - */ - public IdDt(String theResourceType, BigDecimal theIdPart) { - this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart)); - } - - /** - * Constructor - * - * @param theResourceType - * The resource type (e.g. "Patient") - * @param theId - * The ID (e.g. "123") - */ - public IdDt(String theResourceType, String theId) { - this(theResourceType, theId, null); - } - - /** - * Constructor - * - * @param theResourceType - * The resource type (e.g. "Patient") - * @param theId - * The ID (e.g. "123") - * @param theVersionId - * The version ID ("e.g. "456") - */ - public IdDt(String theResourceType, String theId, String theVersionId) { - this(null,theResourceType,theId,theVersionId); - } - - /** - * Constructor - * - * @param theBaseUrl - * The server base URL (e.g. "http://example.com/fhir") - * @param theResourceType - * The resource type (e.g. "Patient") - * @param theId - * The ID (e.g. "123") - * @param theVersionId - * The version ID ("e.g. "456") - */ - public IdDt(String theBaseUrl, String theResourceType, String theId, String theVersionId) { - myBaseUrl = theBaseUrl; - myResourceType = theResourceType; - myUnqualifiedId = theId; - myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null); - myHaveComponentParts = true; - } - - /** - * Creates an ID based on a given URL - */ - public IdDt(UriDt theUrl) { - setValue(theUrl.getValueAsString()); - } - - /** - * @deprecated Use {@link #getIdPartAsBigDecimal()} instead (this method was deprocated because its name is ambiguous) - */ - public BigDecimal asBigDecimal() { - return getIdPartAsBigDecimal(); - } - - /** - * Returns true if this IdDt matches the given IdDt in terms of resource type and ID, but ignores the URL base - */ - @SuppressWarnings("deprecation") - public boolean equalsIgnoreBase(IdDt theId) { - if (theId == null) { - return false; - } - if (theId.isEmpty()) { - return isEmpty(); - } - return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart()); - } - - - - @Override - public boolean equals(Object theArg0) { - if (!(theArg0 instanceof IdDt)) { - return false; - } - IdDt id = (IdDt)theArg0; - return StringUtils.equals(getValueAsString(), id.getValueAsString()); - } - - @Override - public int hashCode() { - HashCodeBuilder b = new HashCodeBuilder(); - b.append(getValueAsString()); - return b.toHashCode(); - } - - /** - * Returns the portion of this resource ID which corresponds to the server base URL. For example given the resource ID http://example.com/fhir/Patient/123 the base URL would be - * http://example.com/fhir. - *

- * This method may return null if the ID contains no base (e.g. "Patient/123") - *

- */ - public String getBaseUrl() { - return myBaseUrl; - } - - public String getIdPart() { - return myUnqualifiedId; - } - - /** - * Returns the unqualified portion 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 getIdPartAsBigDecimal() { - String val = getIdPart(); - if (isBlank(val)) { - return null; - } - return new BigDecimal(val); - } - - /** - * Returns the unqualified portion of this ID as a {@link Long}, or null if the value is null - * - * @throws NumberFormatException - * If the value is not a valid Long - */ - public Long getIdPartAsLong() { - String val = getIdPart(); - if (isBlank(val)) { - return null; - } - return Long.parseLong(val); - } - - public String getResourceType() { - return myResourceType; - } - - /** - * Returns the value of this ID. Note that this value may be a fully qualified URL, a relative/partial URL, or a simple ID. Use {@link #getIdPart()} to get just the ID portion. - * - * @see #getIdPart() - */ - @Override - public String getValue() { - if (myValue == null && myHaveComponentParts) { - StringBuilder b = new StringBuilder(); - if (isNotBlank(myBaseUrl)) { - b.append(myBaseUrl); - if (myBaseUrl.charAt(myBaseUrl.length()-1)!='/') { - b.append('/'); - } - } - - if (isNotBlank(myResourceType)) { - b.append(myResourceType); - } - - if (b.length() > 0) { - b.append('/'); - } - - b.append(myUnqualifiedId); - if (isNotBlank(myUnqualifiedVersionId)) { - b.append('/'); - b.append(Constants.PARAM_HISTORY); - b.append('/'); - b.append(myUnqualifiedVersionId); - } - myValue = b.toString(); - } - return myValue; - } - - @Override - public String getValueAsString() { - return getValue(); - } - - public String getVersionIdPart() { - return myUnqualifiedVersionId; - } - - public Long getVersionIdPartAsLong() { - if (!hasVersionIdPart()) { - return null; - } else { - return Long.parseLong(getVersionIdPart()); - } - } - - /** - * Returns true if this ID has a base url - * - * @see #getBaseUrl() - */ - public boolean hasBaseUrl() { - return isNotBlank(myBaseUrl); - } - - public boolean hasIdPart() { - return isNotBlank(getIdPart()); - } - - public boolean hasResourceType() { - return isNotBlank(myResourceType); - } - - public boolean hasVersionIdPart() { - return isNotBlank(getVersionIdPart()); - } - - /** - * Returns true if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://" - */ - public boolean isAbsolute() { - if (StringUtils.isBlank(getValue())) { - return false; - } - return UrlUtil.isAbsolute(getValue()); - } - - /** - * Returns true if the unqualified ID is a valid {@link Long} value (in other words, it consists only of digits) - */ - public boolean isIdPartValidLong() { - String id = getIdPart(); - if (StringUtils.isBlank(id)) { - return false; - } - for (int i = 0; i < id.length(); i++) { - if (Character.isDigit(id.charAt(i)) == false) { - return false; - } - } - return true; - } - - /** - * Returns true if the ID is a local reference (in other words, it begins with the '#' character) - */ - public boolean isLocal() { - return myUnqualifiedId != null && myUnqualifiedId.isEmpty() == false && myUnqualifiedId.charAt(0) == '#'; - } - - /** - * Copies the value from the given IdDt to this IdDt. It is generally not neccesary to use this method but it is provided for consistency with the rest of the API. - */ - public void setId(IdDt theId) { - setValue(theId.getValue()); - } - - /** - * Set the value - * - *

- * Description: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length - * limit of 36 characters. - *

- *

- * regex: [a-z0-9\-\.]{1,36} - *

- * @return - */ - @Override - public IdDt setValue(String theValue) throws DataFormatException { - // TODO: add validation - myValue = theValue; - myHaveComponentParts = false; - if (StringUtils.isBlank(theValue)) { - myValue = null; - myUnqualifiedId = null; - myUnqualifiedVersionId = null; - myResourceType = null; - } else { - int vidIndex = theValue.indexOf("/_history/"); - int idIndex; - if (vidIndex != -1) { - myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length()); - idIndex = theValue.lastIndexOf('/', vidIndex - 1); - myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex); - } else { - idIndex = theValue.lastIndexOf('/'); - myUnqualifiedId = theValue.substring(idIndex + 1); - myUnqualifiedVersionId = null; - } - - myBaseUrl = null; - if (idIndex <= 0) { - myResourceType = null; - } else { - int typeIndex = theValue.lastIndexOf('/', idIndex - 1); - if (typeIndex == -1) { - myResourceType = theValue.substring(0, idIndex); - } else { - myResourceType = theValue.substring(typeIndex + 1, idIndex); - - if (typeIndex > 4) { - myBaseUrl = theValue.substring(0, typeIndex); - } - - } - } - - } - return this; - } - - /** - * Set the value - * - *

- * Description: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length - * limit of 36 characters. - *

- *

- * regex: [a-z0-9\-\.]{1,36} - *

- */ - @Override - public void setValueAsString(String theValue) throws DataFormatException { - setValue(theValue); - } - - @Override - public String toString() { - return getValue(); - } - - public IdDt toUnqualified() { - return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); - } - - public IdDt toUnqualifiedVersionless() { - return new IdDt(getResourceType(), getIdPart()); - } - - public IdDt toVersionless() { - String value = getValue(); - int i = value.indexOf(Constants.PARAM_HISTORY); - if (i > 1) { - return new IdDt(value.substring(0, i - 1)); - } else { - return this; - } - } - - public IdDt withResourceType(String theResourceName) { - return new IdDt(theResourceName, getIdPart(), getVersionIdPart()); - } - - /** - * Returns a view of this ID as a fully qualified URL, given a server base and resource name (which will only be used if the ID does not already contain those respective parts). Essentially, - * because IdDt can contain either a complete URL or a partial one (or even jut a simple ID), this method may be used to translate into a complete URL. - * - * @param theServerBase - * The server base (e.g. "http://example.com/fhir") - * @param theResourceType - * The resource name (e.g. "Patient") - * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1") - */ - public String withServerBase(String theServerBase, String theResourceType) { - if (getValue().startsWith("http")) { - return getValue(); - } - StringBuilder retVal = new StringBuilder(); - retVal.append(theServerBase); - if (retVal.charAt(retVal.length() - 1) != '/') { - retVal.append('/'); - } - if (isNotBlank(getResourceType())) { - retVal.append(getResourceType()); - } else { - retVal.append(theResourceType); - } - retVal.append('/'); - retVal.append(getIdPart()); - - if (hasVersionIdPart()) { - retVal.append('/'); - retVal.append(Constants.PARAM_HISTORY); - retVal.append('/'); - retVal.append(getVersionIdPart()); - } - - return retVal.toString(); - } - - /** - * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion. - * - * @param theVersion - * The actual version string, e.g. "1" - * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion. - */ - public IdDt withVersion(String theVersion) { - Validate.notBlank(theVersion, "Version may not be null or empty"); - - String existingValue = getValue(); - - int i = existingValue.indexOf(Constants.PARAM_HISTORY); - String value; - if (i > 1) { - value = existingValue.substring(0, i - 1); - } else { - value = existingValue; - } - - return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion); - } - - private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) { - if (theIdPart == null) { - throw new NullPointerException("BigDecimal ID can not be null"); - } - return theIdPart.toPlainString(); - } - - @Override - public boolean isEmpty() { - return isBlank(getValue()); - } - - public void applyTo(IBaseResource theResouce) { - if (theResouce == null) { - throw new NullPointerException("theResource can not be null"); - } else if (theResouce instanceof IResource) { - ((IResource) theResouce).setId(new IdDt(getValue())); - } else if (theResouce instanceof Resource) { - ((Resource) theResouce).setId(getIdPart()); - } else { - throw new IllegalArgumentException("Unknown resource class type, does not implement IResource or extend Resource"); - } - } - -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 91992796735..f04ec211933 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -65,11 +65,16 @@ import ca.uhn.fhir.rest.gclient.IGetPageTyped; import ca.uhn.fhir.rest.gclient.IGetTags; import ca.uhn.fhir.rest.gclient.IParam; import ca.uhn.fhir.rest.gclient.IQuery; +import ca.uhn.fhir.rest.gclient.IRead; +import ca.uhn.fhir.rest.gclient.IReadExecutable; +import ca.uhn.fhir.rest.gclient.IReadIfNoneMatch; +import ca.uhn.fhir.rest.gclient.IReadTyped; import ca.uhn.fhir.rest.gclient.ISort; import ca.uhn.fhir.rest.gclient.ITransaction; import ca.uhn.fhir.rest.gclient.ITransactionTyped; import ca.uhn.fhir.rest.gclient.IUntypedQuery; import ca.uhn.fhir.rest.gclient.IUpdate; +import ca.uhn.fhir.rest.gclient.IUpdateExecutable; import ca.uhn.fhir.rest.gclient.IUpdateTyped; import ca.uhn.fhir.rest.method.DeleteMethodBinding; import ca.uhn.fhir.rest.method.HistoryMethodBinding; @@ -87,6 +92,8 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; +import ca.uhn.fhir.util.ICallable; /** * @author James Agnew @@ -121,7 +128,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") Class conformance = (Class) myContext.getResourceDefinition("Conformance").getImplementingClass(); - + ResourceResponseHandler binding = new ResourceResponseHandler(conformance, null); BaseConformance resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); return resp; @@ -232,23 +239,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public T read(final Class theType, IdDt theId) { - if (theId == null || theId.hasIdPart() == false) { - throw new IllegalArgumentException("theId does not contain a valid ID, is: " + theId); - } - - HttpGetClientInvocation invocation; - if (theId.hasBaseUrl()) { - invocation = ReadMethodBinding.createAbsoluteReadInvocation(theId.toVersionless()); - } else { - invocation = ReadMethodBinding.createReadInvocation(theId, toResourceName(theType)); - } - if (isKeepResponses()) { - myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding()); - } - - ResourceResponseHandler binding = new ResourceResponseHandler(theType, theId); - T resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); - return resp; + return doReadOrVRead(theType, theId, false, null, null); } @Override @@ -420,30 +411,54 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public T vread(final Class theType, IdDt theId) { + public T vread(final Class theType, IdDt theId) { + if (theId.hasVersionIdPart() == false) { + throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_NO_VERSION_ID_FOR_VREAD, theId.getValue())); + } + return doReadOrVRead(theType, theId, true, null, null); + } + + private T doReadOrVRead(final Class theType, IdDt theId, boolean theVRead, ICallable theNotModifiedHandler, String theIfVersionMatches) { String resName = toResourceName(theType); IdDt id = theId; if (!id.hasBaseUrl()) { id = new IdDt(resName, id.getIdPart(), id.getVersionIdPart()); } - if (id.hasVersionIdPart() == false) { - throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_NO_VERSION_ID_FOR_VREAD, theId.getValue())); - } - HttpGetClientInvocation invocation; if (id.hasBaseUrl()) { - invocation = ReadMethodBinding.createAbsoluteVReadInvocation(id); + if (theVRead) { + invocation = ReadMethodBinding.createAbsoluteVReadInvocation(id); + } else { + invocation = ReadMethodBinding.createAbsoluteReadInvocation(id); + } } else { - invocation = ReadMethodBinding.createVReadInvocation(id); + if (theVRead) { + invocation = ReadMethodBinding.createVReadInvocation(id, resName); + } else { + invocation = ReadMethodBinding.createReadInvocation(id, resName); + } } if (isKeepResponses()) { myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding()); } + if (theIfVersionMatches != null) { + invocation.addHeader(Constants.HEADER_IF_NONE_MATCH, '"' + theIfVersionMatches + '"'); + } + ResourceResponseHandler binding = new ResourceResponseHandler(theType, id); - T resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); - return resp; + + if (theNotModifiedHandler == null) { + return invokeClient(myContext, binding, invocation, myLogRequestAndResponse); + } else { + try { + return invokeClient(myContext, binding, invocation, myLogRequestAndResponse); + } catch (NotModifiedException e) { + return theNotModifiedHandler.call(); + } + } + } /* also deprecated in interface */ @@ -454,7 +469,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public T vread(Class theType, String theId, String theVersionId) { + public T vread(Class theType, String theId, String theVersionId) { IdDt resId = new IdDt(toResourceName(theType), theId, theVersionId); return vread(theType, resId); } @@ -549,8 +564,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); @@ -577,7 +591,7 @@ public class GenericClient extends BaseClient implements IGenericClient { if (getParamEncoding() != null) { myResourceBody = null; } - + BaseHttpClientInvocation invocation = MethodUtil.createCreateInvocation(myResource, myResourceBody, myId, myContext); RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource); @@ -688,6 +702,132 @@ public class GenericClient extends BaseClient implements IGenericClient { } + @Override + public IRead read() { + return new ReadInternal(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private class ReadInternal extends BaseClientExecutable implements IRead, IReadTyped, IReadExecutable { + private RuntimeResourceDefinition myType; + private IdDt myId; + private ICallable myNotModifiedHandler; + private String myIfVersionMatches; + + @Override + public IReadTyped resource(Class theResourceType) { + Validate.notNull(theResourceType, "theResourceType must not be null"); + myType = myContext.getResourceDefinition(theResourceType); + if (myType == null) { + throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_CANNOT_DETEMINE_RESOURCE_TYPE, theResourceType)); + } + return this; + } + + @Override + public IReadTyped resource(String theResourceAsText) { + Validate.notBlank(theResourceAsText, "You must supply a value for theResourceAsText"); + myType = myContext.getResourceDefinition(theResourceAsText); + if (myType == null) { + throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_CANNOT_DETEMINE_RESOURCE_TYPE, theResourceAsText)); + } + return this; + } + + @Override + public IReadExecutable withId(String theId) { + Validate.notBlank(theId, "The ID can not be blank"); + myId = new IdDt(myType.getName(), theId); + return this; + } + + @Override + public IReadExecutable withIdAndVersion(String theId, String theVersion) { + Validate.notBlank(theId, "The ID can not be blank"); + myId = new IdDt(myType.getName(), theId, theVersion); + return this; + } + + @Override + public IReadExecutable withId(IdDt theId) { + Validate.notNull(theId, "The ID can not be null"); + Validate.notBlank(theId.getIdPart(), "The ID can not be blank"); + myId = theId.toUnqualified(); + return this; + } + + @Override + public IReadExecutable withUrl(String theUrl) { + myId = new IdDt(theUrl); + processUrl(); + return this; + } + + private void processUrl() { + String resourceType = myId.getResourceType(); + if (isBlank(resourceType)) { + throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_INCOMPLETE_URI_FOR_READ, myId)); + } + myType = myContext.getResourceDefinition(resourceType); + if (myType == null) { + throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_CANNOT_DETEMINE_RESOURCE_TYPE, myId)); + } + } + + @Override + public Object execute() { + if (myId.hasVersionIdPart()) { + return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches); + } else { + return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches); + } + } + + @Override + public IReadExecutable withUrl(IdDt theUrl) { + Validate.notNull(theUrl, "theUrl can not be null"); + myId = theUrl; + processUrl(); + return this; + } + + @Override + public IReadIfNoneMatch ifVersionMatches(String theVersion) { + myIfVersionMatches = theVersion; + return new IReadIfNoneMatch() { + + @Override + public IReadExecutable returnResource(final IBaseResource theInstance) { + myNotModifiedHandler = new ICallable() { + @Override + public Object call() { + return theInstance; + } + }; + return ReadInternal.this; + } + + @Override + public IReadExecutable returnNull() { + myNotModifiedHandler = new ICallable() { + @Override + public Object call() { + return null; + } + }; + return ReadInternal.this; + } + + @Override + public IReadExecutable throwNotModifiedException() { + myNotModifiedHandler = null; + return ReadInternal.this; + } + }; + } + + } + private class GetTagsInternal extends BaseClientExecutable implements IGetTags { private String myId; @@ -776,8 +916,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private final class OperationOutcomeResponseHandler implements IClientResponseHandler { @Override - public BaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public BaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { return null; @@ -791,7 +930,7 @@ public class GenericClient extends BaseClient implements IGenericClient { ourLog.warn("Failed to parse OperationOutcome response", e); return null; } - MethodUtil.parseClientRequestResourceHeaders(theHeaders, retVal); + MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, retVal); return retVal; } @@ -805,8 +944,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) { response.setCreated(true); @@ -824,8 +962,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { return new BundleResponseHandler(myType).invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders).toListOfResources(); } } @@ -849,11 +986,7 @@ public class GenericClient extends BaseClient implements IGenericClient { IParser parser = respType.newParser(myContext); T retVal = parser.parseResource(myType, theResponseReader); - if (myId != null) { - myId.applyTo(retVal); - } - - MethodUtil.parseClientRequestResourceHeaders(theHeaders, retVal); + MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); return retVal; } @@ -1035,8 +1168,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private final class TagListResponseHandler implements IClientResponseHandler { @Override - public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); @@ -1091,7 +1223,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private class UpdateInternal extends BaseClientExecutable implements IUpdate, IUpdateTyped { + private class UpdateInternal extends BaseClientExecutable implements IUpdate, IUpdateTyped, IUpdateExecutable { private IdDt myId; private IResource myResource; @@ -1112,7 +1244,7 @@ public class GenericClient extends BaseClient implements IGenericClient { // If an explicit encoding is chosen, we will re-serialize to ensure the right encoding if (getParamEncoding() != null) { myResourceBody = null; - } + } BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(myResource, myResourceBody, myId, myContext); @@ -1141,7 +1273,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public IUpdateTyped withId(IdDt theId) { + public IUpdateExecutable withId(IdDt theId) { if (theId == null) { throw new NullPointerException("theId can not be null"); } @@ -1153,7 +1285,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public IUpdateTyped withId(String theId) { + public IUpdateExecutable withId(String theId) { if (theId == null) { throw new NullPointerException("theId can not be null"); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java index dd728a00352..1f310b727ca 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java @@ -38,12 +38,13 @@ import ca.uhn.fhir.rest.gclient.ICreate; import ca.uhn.fhir.rest.gclient.IDelete; import ca.uhn.fhir.rest.gclient.IGetPage; import ca.uhn.fhir.rest.gclient.IGetTags; +import ca.uhn.fhir.rest.gclient.IRead; import ca.uhn.fhir.rest.gclient.ITransaction; import ca.uhn.fhir.rest.gclient.IUntypedQuery; import ca.uhn.fhir.rest.gclient.IUpdate; public interface IGenericClient { - + /** * Retrieves and returns the server conformance statement */ @@ -103,15 +104,17 @@ public interface IGenericClient { * Implementation of the "history instance" method. * * @param theType - * The type of resource to return the history for, or null to search for history across all resources + * The type of resource to return the history for, or + * null to search for history across all resources * @param theId - * The ID of the resource to return the history for, or null to search for all resource instances. Note that if this param is not null, theType must also not - * be null + * The ID of the resource to return the history for, or null to search for all resource + * instances. Note that if this param is not null, theType must also not be null * @param theSince * If not null, request that the server only return resources updated since this time * @param theLimit - * If not null, request that the server return no more than this number of resources. Note that the server may return less even if more are available, but should not return more - * according to the FHIR specification. + * If not null, request that the server return no more than this number of resources. Note that the + * server may return less even if more are available, but should not return more according to the FHIR + * specification. * @return A bundle containing returned resources */ Bundle history(Class theType, IdDt theIdDt, DateTimeDt theSince, Integer theLimit); @@ -120,38 +123,48 @@ public interface IGenericClient { * Implementation of the "history instance" method. * * @param theType - * The type of resource to return the history for, or null to search for history across all resources + * The type of resource to return the history for, or + * null to search for history across all resources * @param theId - * The ID of the resource to return the history for, or null to search for all resource instances. Note that if this param is not null, theType must also not - * be null + * The ID of the resource to return the history for, or null to search for all resource + * instances. Note that if this param is not null, theType must also not be null * @param theSince * If not null, request that the server only return resources updated since this time * @param theLimit - * If not null, request that the server return no more than this number of resources. Note that the server may return less even if more are available, but should not return more - * according to the FHIR specification. + * If not null, request that the server return no more than this number of resources. Note that the + * server may return less even if more are available, but should not return more according to the FHIR + * specification. * @return A bundle containing returned resources */ Bundle history(Class theType, String theIdDt, DateTimeDt theSince, Integer theLimit); /** - * Loads the previous/next bundle of resources from a paged set, using the link specified in the "link type=next" tag within the atom bundle. + * Loads the previous/next bundle of resources from a paged set, using the link specified in the "link type=next" + * tag within the atom bundle. * * @see Bundle#getLinkNext() */ IGetPage loadPage(); /** - * Implementation of the "instance read" method. This method will only ever do a "read" for the latest version of a given resource instance, even if the ID passed in contains a version. If you - * wish to request a specific version of a resource (the "vread" operation), use {@link #vread(Class, IdDt)} instead. + * Fluent method for "read" and "vread" methods. + */ + IRead read(); + + /** + * Implementation of the "instance read" method. This method will only ever do a "read" for the latest version of a + * given resource instance, even if the ID passed in contains a version. If you wish to request a specific version + * of a resource (the "vread" operation), use {@link #vread(Class, IdDt)} instead. *

- * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the resource type and ID) the server base for the client will be ignored, and the URL - * passed in will be queried. + * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the + * resource type and ID) the server base for the client will be ignored, and the URL passed in will be queried. *

* * @param theType * The type of resource to load * @param theId - * The ID to load, including the resource ID and the resource version ID. Valid values include "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" + * The ID to load, including the resource ID and the resource version ID. Valid values include + * "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" * @return The resource */ T read(Class theType, IdDt theId); @@ -188,7 +201,8 @@ public interface IGenericClient { IResource read(UriDt theUrl); /** - * Register a new interceptor for this client. An interceptor can be used to add additional logging, or add security headers, or pre-process responses, etc. + * Register a new interceptor for this client. An interceptor can be used to add additional logging, or add security + * headers, or pre-process responses, etc. */ void registerInterceptor(IClientInterceptor theInterceptor); @@ -218,8 +232,8 @@ public interface IGenericClient { Bundle search(UriDt theUrl); /** - * If set to true, the client will log all requests and all responses. This is probably not a good production setting since it will result in a lot of extra logging, but it can be - * useful for troubleshooting. + * If set to true, the client will log all requests and all responses. This is probably not a good + * production setting since it will result in a lot of extra logging, but it can be useful for troubleshooting. * * @param theLogRequestAndResponse * Should requests and responses be logged @@ -236,14 +250,16 @@ public interface IGenericClient { * * @param theResources * The resources to create/update in a single transaction - * @return A list of resource stubs (these will not be fully populated) containing IDs and other {@link IResource#getResourceMetadata() metadata} + * @return A list of resource stubs (these will not be fully populated) containing IDs and other + * {@link IResource#getResourceMetadata() metadata} * @deprecated Use {@link #transaction()} * */ List transaction(List theResources); /** - * Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)} + * Remove an intercaptor that was previously registered using + * {@link IRestfulClient#registerInterceptor(IClientInterceptor)} */ void unregisterInterceptor(IClientInterceptor theInterceptor); @@ -286,19 +302,21 @@ public interface IGenericClient { MethodOutcome validate(IResource theResource); /** - * Implementation of the "instance vread" method. Note that this method expects theId to contain a resource ID as well as a version ID, and will fail if it does not. + * Implementation of the "instance vread" method. Note that this method expects theId to contain a + * resource ID as well as a version ID, and will fail if it does not. *

- * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the resource type and ID) the server base for the client will be ignored, and the URL - * passed in will be queried. + * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the + * resource type and ID) the server base for the client will be ignored, and the URL passed in will be queried. *

* * @param theType * The type of resource to load * @param theId - * The ID to load, including the resource ID and the resource version ID. Valid values include "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" + * The ID to load, including the resource ID and the resource version ID. Valid values include + * "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" * @return The resource */ - T vread(Class theType, IdDt theId); + T vread(Class theType, IdDt theId); /** * Implementation of the "instance vread" method. @@ -325,6 +343,6 @@ public interface IGenericClient { * The version ID * @return The resource */ - T vread(Class theType, String theId, String theVersionId); + T vread(Class theType, String theId, String theVersionId); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IRead.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IRead.java new file mode 100644 index 00000000000..3c83e43dff4 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IRead.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.gclient; + +import org.hl7.fhir.instance.model.IBaseResource; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IRead { + /** + * Construct a read for the given resource type (e.g. Patient.class) + */ + IReadTyped resource(Class theResourceType); + + /** + * Construct a read for the given resource type (e.g. "Patient") + */ + IReadTyped resource(String theResourceType); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadExecutable.java new file mode 100644 index 00000000000..29c2efc837a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadExecutable.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.rest.gclient; + +import org.hl7.fhir.instance.model.IBaseResource; + +public interface IReadExecutable extends IClientExecutable, T>{ + + /** + * Send an "If-None-Match" header containing theVersion, which requests + * that the server return an "HTTP 301 Not Modified" if the newest version of the resource + * on the server has the same version as the version ID specified by theVersion. + * In this case, the client operation will perform the linked operation. + *

+ * See the ETag Documentation + * for more information. + *

+ * @param theVersion The version ID (e.g. "123") + */ + IReadIfNoneMatch ifVersionMatches(String theVersion); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadIfNoneMatch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadIfNoneMatch.java new file mode 100644 index 00000000000..aec68276e3b --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadIfNoneMatch.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.rest.gclient; + +import org.hl7.fhir.instance.model.IBaseResource; + +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; + +public interface IReadIfNoneMatch { + + /** + * If the server responds with an HTTP 301 Not Modified, + * return the given instance. + */ + IReadExecutable returnResource(T theInstance); + + /** + * If the server responds with an HTTP 301 Not Modified, + * return null. + */ + IReadExecutable returnNull(); + + /** + * If the server responds with an HTTP 301 Not Modified, + * throw a {@link NotModifiedException}. + */ + IReadExecutable throwNotModifiedException(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadTyped.java new file mode 100644 index 00000000000..1d36d4d8046 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IReadTyped.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.rest.gclient; + +import org.hl7.fhir.instance.model.IBaseResource; + +import ca.uhn.fhir.model.primitive.IdDt; + +public interface IReadTyped { + + IReadExecutable withId(String theId); + + IReadExecutable withIdAndVersion(String theId, String theVersion); + + /** + * Search using an ID. Note that even if theId contains a base URL it will be + * ignored in favour of the base url for the given client. If you want to specify + * an absolute URL including a base and have that base used instead, use + * {@link #withUrl(IdDt)} + */ + IReadExecutable withId(IdDt theId); + + IReadExecutable withUrl(String theUrl); + + IReadExecutable withUrl(IdDt theUrl); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateExecutable.java new file mode 100644 index 00000000000..6f84627bd84 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateExecutable.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.rest.api.MethodOutcome; + +public interface IUpdateExecutable extends IClientExecutable{ + + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateTyped.java index 3e44d0117a1..55226c2ff75 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateTyped.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IUpdateTyped.java @@ -21,11 +21,11 @@ package ca.uhn.fhir.rest.gclient; */ import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.MethodOutcome; -public interface IUpdateTyped extends IClientExecutable { +public interface IUpdateTyped extends IUpdateExecutable { - IUpdateTyped withId(IdDt theId); + IUpdateExecutable withId(IdDt theId); + + IUpdateExecutable withId(String theId); - IUpdateTyped withId(String theId); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java index 6666c882483..b84d89360ee 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java @@ -180,7 +180,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding> theHeaders, IBaseResource resource) { + public static void parseClientRequestResourceHeaders(IdDt theRequestedId, Map> theHeaders, IBaseResource resource) { List lmHeaders = theHeaders.get(Constants.HEADER_LAST_MODIFIED_LOWERCASE); if (lmHeaders != null && lmHeaders.size() > 0 && StringUtils.isNotBlank(lmHeaders.get(0))) { String headerValue = lmHeaders.get(0); @@ -132,7 +137,7 @@ public class MethodUtil { if (resource instanceof IResource) { InstantDt lmValue = new InstantDt(headerDateValue); ((IResource) resource).getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, lmValue); - } else if (resource instanceof Resource){ + } else if (resource instanceof Resource) { ((Resource) resource).getMeta().setLastUpdated(headerDateValue); } } catch (Exception e) { @@ -148,6 +153,27 @@ public class MethodUtil { } } + IdDt existing = IdDt.of(resource); + + List eTagHeaders = theHeaders.get(Constants.HEADER_ETAG_LC); + String eTagVersion = null; + if (eTagHeaders != null && eTagHeaders.size() > 0) { + eTagVersion = parseETagValue(eTagHeaders.get(0)); + } + if (isNotBlank(eTagVersion)) { + if (existing == null || existing.isEmpty()) { + if (theRequestedId != null) { + theRequestedId.withVersion(eTagVersion).applyTo(resource); + } + } else if (existing.hasVersionIdPart() == false) { + existing.withVersion(eTagVersion).applyTo(resource); + } + } else if (existing == null || existing.isEmpty()) { + if (theRequestedId != null) { + theRequestedId.applyTo(resource); + } + } + List categoryHeaders = theHeaders.get(Constants.HEADER_CATEGORY_LC); if (categoryHeaders != null && categoryHeaders.size() > 0 && StringUtils.isNotBlank(categoryHeaders.get(0))) { TagList tagList = new TagList(); @@ -165,6 +191,27 @@ public class MethodUtil { } } + public static String parseETagValue(String value) { + String eTagVersion; + value = value.trim(); + if (value.length() > 1) { + if (value.charAt(value.length() - 1) == '"') { + if (value.charAt(0) == '"') { + eTagVersion = value.substring(1, value.length() - 1); + } else if (value.length() > 3 && value.charAt(0) == 'W' && value.charAt(1) == '/' && value.charAt(2) == '"') { + eTagVersion = value.substring(3, value.length() - 1); + } else { + eTagVersion = value; + } + } else { + eTagVersion = value; + } + } else { + eTagVersion = value; + } + return eTagVersion; + } + public static void parseTagValue(TagList tagList, String nextTagComplete) { StringBuilder next = new StringBuilder(nextTagComplete); parseTagValue(tagList, nextTagComplete, next); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java index 53148047192..119a169aae9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java @@ -44,11 +44,13 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; public class ReadMethodBinding extends BaseResourceReturningMethodBinding implements IClientResponseHandlerHandlesBinary { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class); @@ -143,7 +145,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem if (myVersionIdIndex == null) { String resourceName = getResourceName(); if (id.hasVersionIdPart()) { - retVal = createVReadInvocation(new IdDt(resourceName, id.getIdPart(), id.getVersionIdPart())); + retVal = createVReadInvocation(new IdDt(resourceName, id.getIdPart(), id.getVersionIdPart()), resourceName); } else { retVal = createReadInvocation(id, resourceName); } @@ -151,7 +153,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem IdDt vid = ((IdDt) theArgs[myVersionIdIndex]); String resourceName = getResourceName(); - retVal = createVReadInvocation(new IdDt(resourceName, id.getIdPart(), vid.getVersionIdPart())); + retVal = createVReadInvocation(new IdDt(resourceName, id.getIdPart(), vid.getVersionIdPart()), resourceName); } for (int idx = 0; idx < theArgs.length; idx++) { @@ -163,8 +165,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem } @Override - public Object invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException { + public Object invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { byte[] contents = IOUtils.toByteArray(theResponseReader); Binary resource = new Binary(theResponseMimeType, contents); @@ -190,8 +191,25 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem } Object response = invokeServerMethod(theMethodParams); + IBundleProvider retVal = toResourceList(response); - return toResourceList(response); + if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) { + String ifNoneMatch = ((Request)theRequest).getServletRequest().getHeader(Constants.HEADER_IF_NONE_MATCH_LC); + if (retVal.size() == 1 && StringUtils.isNotBlank(ifNoneMatch)) { + List responseResources = retVal.getResources(0, 1); + IResource responseResource = responseResources.get(0); + + ifNoneMatch = MethodUtil.parseETagValue(ifNoneMatch); + if (responseResource.getId() != null && responseResource.getId().hasVersionIdPart()) { + if (responseResource.getId().getVersionIdPart().equals(ifNoneMatch)) { + ourLog.debug("Returning HTTP 301 because request specified {}={}", Constants.HEADER_IF_NONE_MATCH, ifNoneMatch); + throw new NotModifiedException("Not Modified"); + } + } + } + } + + return retVal; } @Override @@ -215,8 +233,8 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem return new HttpGetClientInvocation(new IdDt(theResourceName, theId.getIdPart()).getValue()); } - public static HttpGetClientInvocation createVReadInvocation(IdDt theId) { - return new HttpGetClientInvocation(theId.getValue()); + public static HttpGetClientInvocation createVReadInvocation(IdDt theId, String theResourceName) { + return new HttpGetClientInvocation(new IdDt(theResourceName, theId.getIdPart(), theId.getVersionIdPart()).getValue()); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java index 63ec484d6cb..14f0c247d66 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java @@ -25,21 +25,25 @@ import java.util.Map; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.server.RestfulServer; public class RequestDetails { private String myCompartmentName; + private String myCompleteUrl; private IdDt myId; private OtherOperationTypeEnum myOtherOperationType; private Map myParameters; private String myResourceName; private RestfulOperationTypeEnum myResourceOperationType; + private RestfulServer myServer; private RestfulOperationSystemEnum mySystemOperationType; - private String myCompleteUrl; - public String getCompartmentName() { return myCompartmentName; } + public String getCompleteUrl() { + return myCompleteUrl; + } public IdDt getId() { return myId; @@ -61,6 +65,10 @@ public class RequestDetails { return myResourceOperationType; } + public RestfulServer getServer() { + return myServer; + } + public RestfulOperationSystemEnum getSystemOperationType() { return mySystemOperationType; } @@ -69,6 +77,10 @@ public class RequestDetails { myCompartmentName = theCompartmentName; } + public void setCompleteUrl(String theCompleteUrl) { + myCompleteUrl = theCompleteUrl; + } + public void setId(IdDt theId) { myId = theId; } @@ -89,16 +101,12 @@ public class RequestDetails { myResourceOperationType = theResourceOperationType; } + public void setServer(RestfulServer theServer) { + myServer = theServer; + } + public void setSystemOperationType(RestfulOperationSystemEnum theSystemOperationType) { mySystemOperationType = theSystemOperationType; } - public String getCompleteUrl() { - return myCompleteUrl; - } - - public void setCompleteUrl(String theCompleteUrl) { - myCompleteUrl = theCompleteUrl; - } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index 8108bba169b..c7875441999 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -72,19 +72,30 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP * Content-Location header, but we allow it in the PUT URL as well.. */ String locationHeader = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_LOCATION); - IdDt id = new IdDt(locationHeader); - if (isNotBlank(id.getResourceType())) { - if (!getResourceName().equals(id.getResourceType())) { - throw new InvalidRequestException("Attempting to update '" + getResourceName() + "' but content-location header specifies different resource type '" + id.getResourceType() + "' - header value: " + locationHeader); + IdDt id = theRequest.getId(); + if (isNotBlank(locationHeader)) { + id = new IdDt(locationHeader); + if (isNotBlank(id.getResourceType())) { + if (!getResourceName().equals(id.getResourceType())) { + throw new InvalidRequestException("Attempting to update '" + getResourceName() + "' but content-location header specifies different resource type '" + id.getResourceType() + "' - header value: " + locationHeader); + } } } + String ifMatchValue = theRequest.getServletRequest().getHeader(Constants.HEADER_IF_MATCH); + if (isNotBlank(ifMatchValue)) { + ifMatchValue = MethodUtil.parseETagValue(ifMatchValue); + if (id != null && id.hasVersionIdPart() == false) { + id = id.withVersion(ifMatchValue); + } + } + if (theRequest.getId() != null && theRequest.getId().hasVersionIdPart() == false) { if (id != null && id.hasVersionIdPart()) { theRequest.setId(id); } } - + if (isNotBlank(locationHeader)) { MethodOutcome mo = new MethodOutcome(); parseContentLocation(mo, getResourceName(), locationHeader); @@ -114,7 +125,7 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP } FhirContext context = getContext(); - HttpPutClientInvocation retVal = MethodUtil.createUpdateInvocation(theResource, null,idDt, context); + HttpPutClientInvocation retVal = MethodUtil.createUpdateInvocation(theResource, null, idDt, context); for (int idx = 0; idx < theArgs.length; idx++) { IParameter nextParam = getParameters().get(idx); @@ -124,28 +135,13 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP return retVal; } - - /* - @Override - public boolean incomingServerRequestMatchesMethod(Request theRequest) { - if (super.incomingServerRequestMatchesMethod(theRequest)) { - if (myVersionIdParameterIndex != null) { - if (theRequest.getVersionId() == null) { - return false; - } - } else { - if (theRequest.getVersionId() != null) { - return false; - } - } - return true; - } else { - return false; - } - } - */ - + * @Override public boolean incomingServerRequestMatchesMethod(Request theRequest) { if + * (super.incomingServerRequestMatchesMethod(theRequest)) { if (myVersionIdParameterIndex != null) { if + * (theRequest.getVersionId() == null) { return false; } } else { if (theRequest.getVersionId() != null) { return + * false; } } return true; } else { return false; } } + */ + @Override protected Set provideAllowableRequestTypes() { return Collections.singleton(RequestType.PUT); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index e3359d09d6f..bccb6c517df 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -44,13 +44,12 @@ public class Constants { public static final Map FORMAT_VAL_TO_ENCODING; public static final Set FORMAT_VAL_XML; public static final String FORMAT_XML = "xml"; - public static final String HEADER_SUFFIX_CT_UTF_8 = "; charset=UTF-8"; public static final String HEADER_ACCEPT = "Accept"; public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; public static final String HEADER_AUTHORIZATION = "Authorization"; - public static final String HEADER_CATEGORY = "Category"; public static final String HEADER_AUTHORIZATION_VALPREFIX_BASIC = "Basic "; public static final String HEADER_AUTHORIZATION_VALPREFIX_BEARER = "Bearer "; + public static final String HEADER_CATEGORY = "Category"; public static final String HEADER_CATEGORY_LC = HEADER_CATEGORY.toLowerCase(); public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; public static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; @@ -61,10 +60,17 @@ public class Constants { public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods"; public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; public static final String HEADER_CORS_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + public static final String HEADER_ETAG = "ETag"; + public static final String HEADER_ETAG_LC = HEADER_ETAG.toLowerCase(); + public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + public static final String HEADER_IF_NONE_MATCH_LC = HEADER_IF_NONE_MATCH.toLowerCase(); + public static final String HEADER_IF_MATCH = "If-Match"; + public static final String HEADER_IF_MATCH_LC = HEADER_IF_MATCH.toLowerCase(); public static final String HEADER_LAST_MODIFIED = "Last-Modified"; public static final String HEADER_LAST_MODIFIED_LOWERCASE = HEADER_LAST_MODIFIED.toLowerCase(); public static final String HEADER_LOCATION = "Location"; public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase(); + public static final String HEADER_SUFFIX_CT_UTF_8 = "; charset=UTF-8"; public static final String HEADERVALUE_CORS_ALLOW_METHODS_ALL = "GET, POST, PUT, DELETE, OPTIONS"; public static final String OPENSEARCH_NS_OLDER = "http://purl.org/atompub/tombstones/1.0"; public static final String PARAM_COUNT = "_count"; @@ -91,6 +97,7 @@ public class Constants { public static final int STATUS_HTTP_200_OK = 200; public static final int STATUS_HTTP_201_CREATED = 201; public static final int STATUS_HTTP_204_NO_CONTENT = 204; + public static final int STATUS_HTTP_304_NOT_MODIFIED = 304; public static final int STATUS_HTTP_400_BAD_REQUEST = 400; public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401; public static final int STATUS_HTTP_404_NOT_FOUND = 404; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ETagSupportEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ETagSupportEnum.java new file mode 100644 index 00000000000..19334acc430 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ETagSupportEnum.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.rest.server; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** + * RESTful server behaviour for automatically adding profile tags + * + * @see RestfulServer#setETagSupport(ETagSupportEnum) + */ +public enum ETagSupportEnum { + /** + * Send ETag headers + */ + ENABLED, + + /** + * Do not send ETag headers + */ + DISABLED +} 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 54eea9c63b1..75714e824cf 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 @@ -39,12 +39,14 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.util.VersionUtil; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.http.client.utils.DateUtils; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; @@ -68,10 +70,15 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class RestfulServer extends HttpServlet { + /** + * Default setting for {@link #setETagSupport(ETagSupportEnum)ETag Support}: {@link ETagSupportEnum#ENABLED} + */ + public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; private AddProfileTagEnum myAddProfileTag; + private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT; private FhirContext myFhirContext; private String myImplementationDescription; private final List myInterceptors = new ArrayList(); @@ -87,6 +94,7 @@ public class RestfulServer extends HttpServlet { /** This is configurable but by default we just use HAPI version */ private String myServerVersion = VersionUtil.getVersion(); private boolean myStarted; + private boolean myUseBrowserFriendlyContentTypes; /** @@ -104,7 +112,8 @@ public class RestfulServer extends HttpServlet { /** * This method is called prior to sending a response to incoming requests. It is used to add custom headers. *

- * Use caution if overriding this method: it is recommended to call super.addHeadersToResponse to avoid inadvertantly disabling functionality. + * Use caution if overriding this method: it is recommended to call super.addHeadersToResponse to avoid + * inadvertantly disabling functionality. *

*/ public void addHeadersToResponse(HttpServletResponse theHttpResponse) { @@ -117,6 +126,15 @@ public class RestfulServer extends HttpServlet { } } + @Override + public void destroy() { + if (getResourceProviders() != null) { + for (IResourceProvider iResourceProvider : getResourceProviders()) { + invokeDestroy(iResourceProvider); + } + } + } + @Override protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(SearchMethodBinding.RequestType.DELETE, request, response); @@ -262,32 +280,6 @@ public class RestfulServer extends HttpServlet { } } - private void invokeDestroy(Object theProvider) { - Class clazz = theProvider.getClass(); - invokeDestroy(theProvider, clazz); - } - - private void invokeDestroy(Object theProvider, Class clazz) { - for (Method m : clazz.getDeclaredMethods()) { - Destroy destroy = m.getAnnotation(Destroy.class); - if (destroy != null) { - try { - m.invoke(theProvider); - } catch (IllegalAccessException e) { - ourLog.error("Exception occurred in destroy ", e); - } catch (InvocationTargetException e) { - ourLog.error("Exception occurred in destroy ", e); - } - return; - } - } - - Class supertype = clazz.getSuperclass(); - if (!Object.class.equals(supertype)) { - invokeDestroy(theProvider, supertype); - } - } - /** * Returns the setting for automatically adding profile tags * @@ -298,8 +290,15 @@ public class RestfulServer extends HttpServlet { } /** - * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to - * creating their own. + * Returns the server support for ETags (will not be null). Default is {@link #DEFAULT_ETAG_SUPPORT} + */ + public ETagSupportEnum getETagSupport() { + return myETagSupport; + } + + /** + * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain + * providers should generally use this context if one is needed, as opposed to creating their own. */ public FhirContext getFhirContext() { return myFhirContext; @@ -341,16 +340,29 @@ public class RestfulServer extends HttpServlet { } /** - * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy} + * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this + * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} */ public IServerAddressStrategy getServerAddressStrategy() { return myServerAddressStrategy; } + public String getServerBaseForRequest(HttpServletRequest theRequest) { + String fhirServerBase; + fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest); + + if (fhirServerBase.endsWith("/")) { + fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); + } + return fhirServerBase; + } + /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement. + * Returns the server conformance provider, which is the provider that is used to generate the server's conformance + * (metadata) statement. *

- * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null if you do not wish to export a conformance statement. + * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null + * if you do not wish to export a conformance statement. *

*/ public Object getServerConformanceProvider() { @@ -358,7 +370,8 @@ public class RestfulServer extends HttpServlet { } /** - * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. + * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, + * but can be helpful to set with something appropriate. * * @see RestfulServer#setServerName(String) */ @@ -371,7 +384,8 @@ public class RestfulServer extends HttpServlet { } /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. + * Gets the server's version, as exported in conformance profiles exported by the server. This is informational + * only, but can be helpful to set with something appropriate. */ public String getServerVersion() { return myServerVersion; @@ -410,8 +424,7 @@ public class RestfulServer extends HttpServlet { NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest); boolean respondGzip = theRequest.isRespondGzip(); - Bundle bundle = createBundleFromBundleProvider(this, theResponse, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser, - narrativeMode, start, count, thePagingAction); + Bundle bundle = createBundleFromBundleProvider(this, theResponse, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser, narrativeMode, start, count, thePagingAction); for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); @@ -437,7 +450,7 @@ public class RestfulServer extends HttpServlet { String fhirServerBase = null; boolean requestIsBrowser = requestIsBrowser(theRequest); - RequestDetails requestDetails=null; + RequestDetails requestDetails = null; try { String resourceName = null; @@ -563,6 +576,7 @@ public class RestfulServer extends HttpServlet { } Request r = new Request(); + r.setServer(this); r.setResourceName(resourceName); r.setId(id); r.setOperation(operation); @@ -613,6 +627,17 @@ public class RestfulServer extends HttpServlet { resourceMethod.invokeServer(this, r); + } catch (NotModifiedException e) { + + for (int i = getInterceptors().size() - 1; i >= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + if (!next.handleException(requestDetails, e, theRequest, theResponse)) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + writeExceptionToResponse(theResponse, e); + } catch (AuthenticationException e) { for (int i = getInterceptors().size() - 1; i >= 0; i--) { @@ -627,17 +652,13 @@ public class RestfulServer extends HttpServlet { // if request is coming from a browser, prompt the user to enter login credentials theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\""); } - theResponse.setStatus(e.getStatusCode()); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().write(e.getMessage()); + writeExceptionToResponse(theResponse, e); } catch (Throwable e) { /* - * We have caught an exception while handling an incoming server request. - * Start by notifying the interceptors.. + * We have caught an exception while handling an incoming server request. Start by notifying the + * interceptors.. */ for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); @@ -656,8 +677,7 @@ public class RestfulServer extends HttpServlet { } /* - * Generate an OperationOutcome to return, unless the exception throw by - * the resource provider had one + * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one */ if (oo == null) { try { @@ -705,19 +725,10 @@ public class RestfulServer extends HttpServlet { } } - public String getServerBaseForRequest(HttpServletRequest theRequest) { - String fhirServerBase; - fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest); - - if (fhirServerBase.endsWith("/")) { - fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); - } - return fhirServerBase; - } - /** - * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, but subclasses may put initialization code in {@link #initialize()}, which is - * called immediately before beginning initialization of the restful server's internal init. + * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, + * but subclasses may put initialization code in {@link #initialize()}, which is called immediately before beginning + * initialization of the restful server's internal init. */ @Override public final void init() throws ServletException { @@ -732,12 +743,12 @@ public class RestfulServer extends HttpServlet { if (resourceProvider != null) { Map typeToProvider = new HashMap(); for (IResourceProvider nextProvider : resourceProvider) { - + Class resourceType = nextProvider.getResourceType(); if (resourceType == null) { throw new NullPointerException("getResourceType() on class '" + nextProvider.getClass().getCanonicalName() + "' returned null"); } - + String resourceName = myFhirContext.getResourceDefinition(resourceType).getName(); if (typeToProvider.containsKey(resourceName)) { throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + typeToProvider.get(resourceName).getClass().getCanonicalName() + "] and Second[" + nextProvider.getClass().getCanonicalName() + "]"); @@ -773,22 +784,40 @@ public class RestfulServer extends HttpServlet { } /** - * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used. + * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the + * server being used. */ protected void initialize() throws ServletException { // nothing by default } - @Override - public void destroy() { - if (getResourceProviders() != null) { - for (IResourceProvider iResourceProvider : getResourceProviders()) { - invokeDestroy(iResourceProvider); - } - } - } + private void invokeDestroy(Object theProvider) { + Class clazz = theProvider.getClass(); + invokeDestroy(theProvider, clazz); + } - public boolean isUseBrowserFriendlyContentTypes() { + private void invokeDestroy(Object theProvider, Class clazz) { + for (Method m : clazz.getDeclaredMethods()) { + Destroy destroy = m.getAnnotation(Destroy.class); + if (destroy != null) { + try { + m.invoke(theProvider); + } catch (IllegalAccessException e) { + ourLog.error("Exception occurred in destroy ", e); + } catch (InvocationTargetException e) { + ourLog.error("Exception occurred in destroy ", e); + } + return; + } + } + + Class supertype = clazz.getSuperclass(); + if (!Object.class.equals(supertype)) { + invokeDestroy(theProvider, supertype); + } + } + + public boolean isUseBrowserFriendlyContentTypes() { return myUseBrowserFriendlyContentTypes; } @@ -803,8 +832,9 @@ public class RestfulServer extends HttpServlet { } /** - * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} (which is the default), the server will automatically add a profile tag based - * on the class of the resource(s) being returned. + * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} + * (which is the default), the server will automatically add a profile tag based on the class of the resource(s) + * being returned. * * @param theAddProfileTag * The behaviour enum (must not be null) @@ -814,6 +844,20 @@ public class RestfulServer extends HttpServlet { myAddProfileTag = theAddProfileTag; } + /** + * Sets (enables/disables) the server support for ETags. Must not be null. Default is + * {@link #DEFAULT_ETAG_SUPPORT} + * + * @param theETagSupport + * The ETag support mode + */ + public void setETagSupport(ETagSupportEnum theETagSupport) { + if (theETagSupport == null) { + throw new NullPointerException("theETagSupport can not be null"); + } + myETagSupport = theETagSupport; + } + public void setFhirContext(FhirContext theFhirContext) { Validate.notNull(theFhirContext, "FhirContext must not be null"); myFhirContext = theFhirContext; @@ -829,10 +873,10 @@ public class RestfulServer extends HttpServlet { * @param theList * The list of interceptors (may be null) */ - public void setInterceptors(List theList) { + public void setInterceptors(IServerInterceptor... theList) { myInterceptors.clear(); if (theList != null) { - myInterceptors.addAll(theList); + myInterceptors.addAll(Arrays.asList(theList)); } } @@ -842,10 +886,10 @@ public class RestfulServer extends HttpServlet { * @param theList * The list of interceptors (may be null) */ - public void setInterceptors(IServerInterceptor... theList) { + public void setInterceptors(List theList) { myInterceptors.clear(); if (theList != null) { - myInterceptors.addAll(Arrays.asList(theList)); + myInterceptors.addAll(theList); } } @@ -898,7 +942,8 @@ public class RestfulServer extends HttpServlet { } /** - * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy} + * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this + * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} */ public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); @@ -906,14 +951,17 @@ public class RestfulServer extends HttpServlet { } /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement. + * Returns the server conformance provider, which is the provider that is used to generate the server's conformance + * (metadata) statement. *

- * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null if you do not wish to export a conformance statement. + * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null + * if you do not wish to export a conformance statement. *

* Note that this method can only be called before the server is initialized. * * @throws IllegalStateException - * Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that. + * Note that this method can only be called prior to {@link #init() initialization} and will throw an + * {@link IllegalStateException} if called after that. */ public void setServerConformanceProvider(Object theServerConformanceProvider) { if (myStarted) { @@ -923,22 +971,24 @@ public class RestfulServer extends HttpServlet { } /** - * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. + * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, + * but can be helpful to set with something appropriate. */ public void setServerName(String theServerName) { myServerName = theServerName; } /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. + * Gets the server's version, as exported in conformance profiles exported by the server. This is informational + * only, but can be helpful to set with something appropriate. */ public void setServerVersion(String theServerVersion) { myServerVersion = theServerVersion; } /** - * If set to true (default is false), the server will use browser friendly content-types (instead of standard FHIR ones) when it detects that the request is coming from a browser - * instead of a FHIR + * If set to true (default is false), the server will use browser friendly content-types (instead of + * standard FHIR ones) when it detects that the request is coming from a browser instead of a FHIR */ public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes; @@ -949,6 +999,14 @@ public class RestfulServer extends HttpServlet { myInterceptors.remove(theInterceptor); } + private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException { + theResponse.setStatus(theException.getStatusCode()); + addHeadersToResponse(theResponse); + theResponse.setContentType("text/plain"); + theResponse.setCharacterEncoding("UTF-8"); + theResponse.getWriter().write(theException.getMessage()); + } + private static void addProfileToBundleEntry(FhirContext theContext, IResource theResource, String theServerBase) { TagList tl = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); @@ -964,8 +1022,8 @@ public class RestfulServer extends HttpServlet { } } - public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding, - String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) { + public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, + NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) { theHttpResponse.setStatus(200); if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { @@ -1049,12 +1107,6 @@ public class RestfulServer extends HttpServlet { return bundle; } - private static void validateResourceListNotNull(List theResourceList) { - if (theResourceList == null) { - throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); - } - } - public static Bundle createBundleFromResourceList(FhirContext theContext, String theAuthor, List theResult, String theServerBase, String theCompleteUrl, int theTotalResults) { Bundle bundle = new Bundle(); bundle.getAuthorName().setValue(theAuthor); @@ -1210,7 +1262,8 @@ public class RestfulServer extends HttpServlet { } /** - * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's "_format" parameter and "Accept:" HTTP header. + * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's + * "_format" parameter and "Accept:" HTTP header. */ public static EncodingEnum determineResponseEncoding(HttpServletRequest theReq) { String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT); @@ -1303,8 +1356,7 @@ public class RestfulServer extends HttpServlet { return prettyPrint; } - public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase, - boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) throws IOException { + public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) throws IOException { assert !theServerBase.endsWith("/"); Writer writer = getWriter(theHttpResponse, theRespondGzip); @@ -1322,14 +1374,14 @@ public class RestfulServer extends HttpServlet { } } - public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, - boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip, String theServerBase) throws IOException { + public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip, String theServerBase) + throws IOException { int stausCode = 200; streamResponseAsResource(theServer, theHttpResponse, theResource, theResponseEncoding, thePrettyPrint, theRequestIsBrowser, theNarrativeMode, stausCode, theRespondGzip, theServerBase); } - private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, - boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode, boolean theRespondGzip, String theServerBase) throws IOException { + private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode, boolean theRespondGzip, + String theServerBase) throws IOException { theHttpResponse.setStatus(stausCode); if (theResource.getId() != null && theResource.getId().hasIdPart() && isNotBlank(theServerBase)) { @@ -1338,6 +1390,12 @@ public class RestfulServer extends HttpServlet { theHttpResponse.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId); } + if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) { + if (theResource.getId().hasVersionIdPart()) { + theHttpResponse.addHeader(Constants.HEADER_ETAG, '"' + theResource.getId().getVersionIdPart() + '"'); + } + } + if (theServer.getAddProfileTag() != AddProfileTagEnum.NEVER) { RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(theResource); if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardProfile()) { @@ -1377,8 +1435,8 @@ public class RestfulServer extends HttpServlet { theServer.addHeadersToResponse(theHttpResponse); InstantDt lastUpdated = ResourceMetadataKeyEnum.UPDATED.get(theResource); - if (lastUpdated != null) { - theHttpResponse.addHeader(Constants.HEADER_LAST_MODIFIED, lastUpdated.getValueAsString()); + if (lastUpdated != null && lastUpdated.isEmpty() == false) { + theHttpResponse.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue())); } TagList list = (TagList) theResource.getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST); @@ -1415,6 +1473,12 @@ public class RestfulServer extends HttpServlet { return count; } + private static void validateResourceListNotNull(List theResourceList) { + if (theResourceList == null) { + throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); + } + } + public enum NarrativeModeEnum { NORMAL, ONLY, SUPPRESS; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig deleted file mode 100644 index dab360850da..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig +++ /dev/null @@ -1,1428 +0,0 @@ -package ca.uhn.fhir.rest.server; - -/* - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.UUID; -import java.util.zip.GZIPOutputStream; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import ca.uhn.fhir.context.ProvidedResourceScanner; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.exception.ExceptionUtils; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.api.Tag; -import ca.uhn.fhir.model.api.TagList; -import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; -import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue; -import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; -import ca.uhn.fhir.model.dstu.resource.Binary; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.method.BaseMethodBinding; -import ca.uhn.fhir.rest.method.ConformanceMethodBinding; -import ca.uhn.fhir.rest.method.OtherOperationTypeEnum; -import ca.uhn.fhir.rest.method.Request; -import ca.uhn.fhir.rest.method.RequestDetails; -import ca.uhn.fhir.rest.method.SearchMethodBinding; -import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; -import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.util.VersionUtil; - -public class RestfulServer extends HttpServlet { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); - private static final long serialVersionUID = 1L; - - private AddProfileTagEnum myAddProfileTag; - private FhirContext myFhirContext; - private String myImplementationDescription; - private final List myInterceptors = new ArrayList(); - private ResourceBinding myNullResourceBinding = new ResourceBinding(); - private IPagingProvider myPagingProvider; - private Collection myPlainProviders; - private Map myResourceNameToProvider = new HashMap(); - private Collection myResourceProviders; - private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy(); - private BaseMethodBinding myServerConformanceMethod; - private Object myServerConformanceProvider; - private String myServerName = "HAPI FHIR Server"; - /** This is configurable but by default we just use HAPI version */ - private String myServerVersion = VersionUtil.getVersion(); - private boolean myStarted; - private boolean myUseBrowserFriendlyContentTypes; - - /** - * Constructor - */ - public RestfulServer() { - this(new FhirContext()); - } - - public RestfulServer(FhirContext theCtx) { - myFhirContext = theCtx; - myServerConformanceProvider = theCtx.getVersion().createServerConformanceProvider(this); - } - - /** - * This method is called prior to sending a response to incoming requests. It is used to add custom headers. - *

- * Use caution if overriding this method: it is recommended to call super.addHeadersToResponse to avoid inadvertantly disabling functionality. - *

- */ - public void addHeadersToResponse(HttpServletResponse theHttpResponse) { - theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); - } - - private void assertProviderIsValid(Object theNext) throws ConfigurationException { - if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) { - throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public"); - } - } - - @Override - protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(SearchMethodBinding.RequestType.DELETE, request, response); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(SearchMethodBinding.RequestType.GET, request, response); - } - - @Override - protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException { - handleRequest(SearchMethodBinding.RequestType.OPTIONS, theReq, theResp); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(SearchMethodBinding.RequestType.POST, request, response); - } - - @Override - protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(SearchMethodBinding.RequestType.PUT, request, response); - } - - /** - * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20) - */ - private int escapedLength(String theServletPath) { - int delta = 0; - for (int i = 0; i < theServletPath.length(); i++) { - char next = theServletPath.charAt(i); - if (next == ' ') { - delta = delta + 2; - } - } - return theServletPath.length() + delta; - } - - private void findResourceMethods(Object theProvider) throws Exception { - - ourLog.info("Scanning type for RESTful methods: {}", theProvider.getClass()); - int count = 0; - - Class clazz = theProvider.getClass(); - Class supertype = clazz.getSuperclass(); - while (!Object.class.equals(supertype)) { - count += findResourceMethods(theProvider, supertype); - supertype = supertype.getSuperclass(); - } - - count += findResourceMethods(theProvider, clazz); - - if (count == 0) { - throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName()); - } - } - - private int findResourceMethods(Object theProvider, Class clazz) throws ConfigurationException { - int count = 0; - - for (Method m : clazz.getDeclaredMethods()) { - BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theProvider); - if (foundMethodBinding == null) { - continue; - } - - count++; - - if (!Modifier.isPublic(m.getModifiers())) { - throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public"); - } else { - if (Modifier.isStatic(m.getModifiers())) { - throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static"); - } else { - ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName()); - - String resourceName = foundMethodBinding.getResourceName(); - ResourceBinding resourceBinding; - if (resourceName == null) { - resourceBinding = myNullResourceBinding; - } else { - RuntimeResourceDefinition definition = myFhirContext.getResourceDefinition(resourceName); - if (myResourceNameToProvider.containsKey(definition.getName())) { - resourceBinding = myResourceNameToProvider.get(definition.getName()); - } else { - resourceBinding = new ResourceBinding(); - resourceBinding.setResourceName(resourceName); - myResourceNameToProvider.put(resourceName, resourceBinding); - } - } - - List> allowableParams = foundMethodBinding.getAllowableParamAnnotations(); - if (allowableParams != null) { - for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) { - for (Annotation annotation : nextParamAnnotations) { - Package pack = annotation.annotationType().getPackage(); - if (pack.equals(IdParam.class.getPackage())) { - if (!allowableParams.contains(annotation.annotationType())) { - throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation); - } - } - } - } - } - - resourceBinding.addMethod(foundMethodBinding); - ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); - } - } - } - - return count; - } - - private void findSystemMethods(Object theSystemProvider) { - Class clazz = theSystemProvider.getClass(); - - findSystemMethods(theSystemProvider, clazz); - - } - - private void findSystemMethods(Object theSystemProvider, Class clazz) { - Class supertype = clazz.getSuperclass(); - if (!Object.class.equals(supertype)) { - findSystemMethods(theSystemProvider, supertype); - } - - for (Method m : clazz.getDeclaredMethods()) { - if (Modifier.isPublic(m.getModifiers())) { - ourLog.debug("Scanning public method: {}#{}", theSystemProvider.getClass(), m.getName()); - - BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theSystemProvider); - if (foundMethodBinding != null) { - if (foundMethodBinding instanceof ConformanceMethodBinding) { - myServerConformanceMethod = foundMethodBinding; - } - ourLog.info(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName()); - } else { - ourLog.debug(" * Method: {}#{} is not a handler", theSystemProvider.getClass(), m.getName()); - } - } - } - } - - /** - * Returns the setting for automatically adding profile tags - * - * @see #setAddProfileTag(AddProfileTagEnum) - */ - public AddProfileTagEnum getAddProfileTag() { - return myAddProfileTag; - } - - /** - * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to - * creating their own. - */ - public FhirContext getFhirContext() { - return myFhirContext; - } - - public String getImplementationDescription() { - return myImplementationDescription; - } - - /** - * Returns a ist of all registered server interceptors - */ - public List getInterceptors() { - return Collections.unmodifiableList(myInterceptors); - } - - public IPagingProvider getPagingProvider() { - return myPagingProvider; - } - - /** - * Provides the non-resource specific providers which implement method calls on this server - * - * @see #getResourceProviders() - */ - public Collection getPlainProviders() { - return myPlainProviders; - } - - public Collection getResourceBindings() { - return myResourceNameToProvider.values(); - } - - /** - * Provides the resource providers for this server - */ - public Collection getResourceProviders() { - return myResourceProviders; - } - - /** - * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy} - */ - public IServerAddressStrategy getServerAddressStrategy() { - return myServerAddressStrategy; - } - - /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement. - *

- * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null if you do not wish to export a conformance statement. - *

- */ - public Object getServerConformanceProvider() { - return myServerConformanceProvider; - } - - /** - * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. - * - * @see RestfulServer#setServerName(String) - */ - public String getServerName() { - return myServerName; - } - - public IResourceProvider getServerProfilesProvider() { - return myFhirContext.getVersion().createServerProfilesProvider(this); - } - - /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. - */ - public String getServerVersion() { - return myServerVersion; - } - - private void handlePagingRequest(Request theRequest, HttpServletResponse theResponse, String thePagingAction) throws IOException { - IBundleProvider resultList = getPagingProvider().retrieveResultList(thePagingAction); - if (resultList == null) { - ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); - theResponse.setStatus(Constants.STATUS_HTTP_410_GONE); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().append("Search ID[" + thePagingAction + "] does not exist and may have expired."); - theResponse.getWriter().close(); - return; - } - - Integer count = extractCountParameter(theRequest.getServletRequest()); - if (count == null) { - count = getPagingProvider().getDefaultPageSize(); - } else if (count > getPagingProvider().getMaximumPageSize()) { - count = getPagingProvider().getMaximumPageSize(); - } - - Integer offsetI = tryToExtractNamedParameter(theRequest.getServletRequest(), Constants.PARAM_PAGINGOFFSET); - if (offsetI == null || offsetI < 0) { - offsetI = 0; - } - - int start = Math.min(offsetI, resultList.size() - 1); - - EncodingEnum responseEncoding = determineResponseEncoding(theRequest.getServletRequest()); - boolean prettyPrint = prettyPrintResponse(theRequest); - boolean requestIsBrowser = requestIsBrowser(theRequest.getServletRequest()); - NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest); - boolean respondGzip = theRequest.isRespondGzip(); - - Bundle bundle = createBundleFromBundleProvider(this, theResponse, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser, - narrativeMode, start, count, thePagingAction); - - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse()); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - streamResponseAsBundle(this, theResponse, bundle, responseEncoding, theRequest.getFhirServerBase(), prettyPrint, narrativeMode, respondGzip); - - } - - protected void handleRequest(SearchMethodBinding.RequestType theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { - for (IServerInterceptor next : myInterceptors) { - boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - String fhirServerBase = null; - boolean requestIsBrowser = requestIsBrowser(theRequest); - RequestDetails requestDetails=null; - try { - - String resourceName = null; - String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); - String servletPath = StringUtils.defaultString(theRequest.getServletPath()); - StringBuffer requestUrl = theRequest.getRequestURL(); - String servletContextPath = ""; - - // if (getServletContext().getMajorVersion() >= 3) { - // // getServletContext is only supported in version 3+ of servlet-api - if (getServletContext() != null) { - servletContextPath = StringUtils.defaultString(getServletContext().getContextPath()); - } - // } - - if (ourLog.isTraceEnabled()) { - ourLog.trace("Request FullPath: {}", requestFullPath); - ourLog.trace("Servlet Path: {}", servletPath); - ourLog.trace("Request Url: {}", requestUrl); - ourLog.trace("Context Path: {}", servletContextPath); - } - - IdDt id = null; - String operation = null; - String compartment = null; - - String requestPath = requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath)); - if (requestPath.length() > 0 && requestPath.charAt(0) == '/') { - requestPath = requestPath.substring(1); - } - - fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest); - - if (fhirServerBase.endsWith("/")) { - fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); - } - - String completeUrl = StringUtils.isNotBlank(theRequest.getQueryString()) ? requestUrl + "?" + theRequest.getQueryString() : requestUrl.toString(); - - Map params = new HashMap(theRequest.getParameterMap()); - - StringTokenizer tok = new StringTokenizer(requestPath, "/"); - if (tok.hasMoreTokens()) { - resourceName = tok.nextToken(); - if (resourceName.startsWith("_")) { - operation = resourceName; - resourceName = null; - } - } - - ResourceBinding resourceBinding = null; - BaseMethodBinding resourceMethod = null; - if ("metadata".equals(resourceName) || theRequestType == RequestType.OPTIONS) { - resourceMethod = myServerConformanceMethod; - } else if (resourceName == null) { - resourceBinding = myNullResourceBinding; - } else { - resourceBinding = myResourceNameToProvider.get(resourceName); - if (resourceBinding == null) { - throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet()); - } - } - - if (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (nextString.startsWith("_")) { - operation = nextString; - } else { - id = new IdDt(resourceName, nextString); - } - } - - if (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (nextString.equals(Constants.PARAM_HISTORY)) { - if (tok.hasMoreTokens()) { - String versionString = tok.nextToken(); - if (id == null) { - throw new InvalidRequestException("Don't know how to handle request path: " + requestPath); - } - id = new IdDt(resourceName + "/" + id.getIdPart() + "/_history/" + versionString); - } else { - operation = Constants.PARAM_HISTORY; - } - } else if (nextString.startsWith("_")) { - if (operation != null) { - throw new InvalidRequestException("URL Path contains two operations (part beginning with _): " + requestPath); - } - operation = nextString; - } else { - compartment = nextString; - } - } - - // Secondary is for things like ..../_tags/_delete - String secondaryOperation = null; - - while (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (operation == null) { - operation = nextString; - } else if (secondaryOperation == null) { - secondaryOperation = nextString; - } else { - throw new InvalidRequestException("URL path has unexpected token '" + nextString + "' at the end: " + requestPath); - } - } - - if (theRequestType == RequestType.PUT) { - String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); - if (contentLocation != null) { - id = new IdDt(contentLocation); - } - } - - // TODO: look for more tokens for version, compartments, etc... - - String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING); - boolean respondGzip = false; - if (acceptEncoding != null) { - String[] parts = acceptEncoding.trim().split("\\s*,\\s*"); - for (String string : parts) { - if (string.equals("gzip")) { - respondGzip = true; - } - } - } - - Request r = new Request(); - r.setResourceName(resourceName); - r.setId(id); - r.setOperation(operation); - r.setSecondaryOperation(secondaryOperation); - r.setParameters(params); - r.setRequestType(theRequestType); - r.setFhirServerBase(fhirServerBase); - r.setCompleteUrl(completeUrl); - r.setServletRequest(theRequest); - r.setServletResponse(theResponse); - r.setRespondGzip(respondGzip); - r.setCompartmentName(compartment); - - String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); - if (getPagingProvider() != null && isNotBlank(pagingAction)) { - r.setOtherOperationType(OtherOperationTypeEnum.GET_PAGE); - handlePagingRequest(r, theResponse, pagingAction); - return; - } - - if (resourceMethod == null && resourceBinding != null) { - resourceMethod = resourceBinding.getMethod(r); - } - if (resourceMethod == null) { - StringBuilder b = new StringBuilder(); - b.append("No resource method available for "); - b.append(theRequestType.name()); - b.append(" operation["); - b.append(requestPath); - b.append("]"); - b.append(" with parameters "); - b.append(params.keySet()); - throw new InvalidRequestException(b.toString()); - } - - requestDetails = r; - requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType()); - requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType()); - requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType()); - - for (IServerInterceptor next : myInterceptors) { - boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - resourceMethod.invokeServer(this, r); - - } catch (AuthenticationException e) { - - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - if (!next.handleException(requestDetails, e, theRequest, theResponse)) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - if (requestIsBrowser) { - // if request is coming from a browser, prompt the user to enter login credentials - theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\""); - } - theResponse.setStatus(e.getStatusCode()); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().write(e.getMessage()); - - } catch (Throwable e) { - -<<<<<<< HEAD - BaseOperationOutcome oo = null; - int statusCode = 500; -======= - /* - * We have caught an exception while handling an incoming server request. - * Start by notifying the interceptors.. - */ - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - if (!next.handleException(requestDetails, e, theRequest, theResponse)) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - BaseOperationOutcome oo = null; - int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR; ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2 - - if (e instanceof BaseServerResponseException) { - oo = ((BaseServerResponseException) e).getOperationOutcome(); - statusCode = ((BaseServerResponseException) e).getStatusCode(); - } - - /* - * Generate an OperationOutcome to return, unless the exception throw by - * the resource provider had one - */ - if (oo == null) { - try { - oo = (BaseOperationOutcome) myFhirContext.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance(); - } catch (Exception e1) { - ourLog.error("Failed to instantiate OperationOutcome resource instance", e1); - throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1); - } -<<<<<<< HEAD - -======= - ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2 - BaseIssue issue = oo.addIssue(); - issue.getSeverityElement().setValue("error"); - if (e instanceof InternalErrorException) { - ourLog.error("Failure during REST processing", e); - issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); -<<<<<<< HEAD - } else if (e instanceof BaseServerResponseException) { - ourLog.warn("Failure during REST processing: {}", e.toString()); - statusCode = ((BaseServerResponseException) e).getStatusCode(); - issue.getDetailsElement().setValue(e.getMessage()); - } else { - ourLog.error("Failure during REST processing", e); - issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); -======= - statusCode = ((InternalErrorException) e).getStatusCode(); - } else if (e instanceof BaseServerResponseException) { - ourLog.warn("Failure during REST processing: {}", e.toString()); - BaseServerResponseException baseServerResponseException = (BaseServerResponseException) e; - statusCode = baseServerResponseException.getStatusCode(); - issue.getDetailsElement().setValue(e.getMessage()); - if (baseServerResponseException.getAdditionalMessages() != null) { - for (String next : baseServerResponseException.getAdditionalMessages()) { - BaseIssue issue2 = oo.addIssue(); - issue2.getSeverityElement().setValue("error"); - issue2.setDetails(next); - } - } - } else { - ourLog.error("Failure during REST processing: " + e.toString(), e); - issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); - statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR; ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2 - } - } - - streamResponseAsResource(this, theResponse, oo, determineResponseEncoding(theRequest), true, requestIsBrowser, NarrativeModeEnum.NORMAL, statusCode, false, fhirServerBase); - - theResponse.setStatus(statusCode); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().append(e.getMessage()); - theResponse.getWriter().close(); - - } - - } - - /** - * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, but subclasses may put initialization code in {@link #initialize()}, which is - * called immediately before beginning initialization of the restful server's internal init. - */ - @Override - public final void init() throws ServletException { - initialize(); - try { - ourLog.info("Initializing HAPI FHIR restful server"); - - ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); - providedResourceScanner.scanForProvidedResources(this); - - Collection resourceProvider = getResourceProviders(); - if (resourceProvider != null) { - Map, IResourceProvider> typeToProvider = new HashMap, IResourceProvider>(); - for (IResourceProvider nextProvider : resourceProvider) { - Class resourceType = nextProvider.getResourceType(); - if (resourceType == null) { - throw new NullPointerException("getResourceType() on class '" + nextProvider.getClass().getCanonicalName() + "' returned null"); - } - if (typeToProvider.containsKey(resourceType)) { - throw new ServletException("Multiple providers for type: " + resourceType.getCanonicalName()); - } - typeToProvider.put(resourceType, nextProvider); - providedResourceScanner.scanForProvidedResources(nextProvider); - } - ourLog.info("Got {} resource providers", typeToProvider.size()); - for (IResourceProvider provider : typeToProvider.values()) { - assertProviderIsValid(provider); - findResourceMethods(provider); - } - } - - Collection providers = getPlainProviders(); - if (providers != null) { - for (Object next : providers) { - assertProviderIsValid(next); - findResourceMethods(next); - } - } - - findResourceMethods(getServerProfilesProvider()); - findSystemMethods(getServerConformanceProvider()); - - } catch (Exception ex) { - ourLog.error("An error occurred while loading request handlers!", ex); - throw new ServletException("Failed to initialize FHIR Restful server", ex); - } - - myStarted = true; - ourLog.info("A FHIR has been lit on this server"); - } - - /** - * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used. - */ - protected void initialize() throws ServletException { - // nothing by default - } - - public boolean isUseBrowserFriendlyContentTypes() { - return myUseBrowserFriendlyContentTypes; - } - - public void registerInterceptor(IServerInterceptor theInterceptor) { - Validate.notNull(theInterceptor, "Interceptor can not be null"); - myInterceptors.add(theInterceptor); - } - - private boolean requestIsBrowser(HttpServletRequest theRequest) { - String userAgent = theRequest.getHeader("User-Agent"); - return userAgent != null && userAgent.contains("Mozilla"); - } - - /** - * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} (which is the default), the server will automatically add a profile tag based - * on the class of the resource(s) being returned. - * - * @param theAddProfileTag - * The behaviour enum (must not be null) - */ - public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) { - Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null"); - myAddProfileTag = theAddProfileTag; - } - - public void setFhirContext(FhirContext theFhirContext) { - Validate.notNull(theFhirContext, "FhirContext must not be null"); - myFhirContext = theFhirContext; - } - - public void setImplementationDescription(String theImplementationDescription) { - myImplementationDescription = theImplementationDescription; - } - - /** - * Sets (or clears) the list of interceptors - * - * @param theList - * The list of interceptors (may be null) - */ - public void setInterceptors(List theList) { - myInterceptors.clear(); - if (theList != null) { - myInterceptors.addAll(theList); - } - } - - /** - * Sets (or clears) the list of interceptors - * - * @param theList - * The list of interceptors (may be null) - */ - public void setInterceptors(IServerInterceptor... theList) { - myInterceptors.clear(); - if (theList != null) { - myInterceptors.addAll(Arrays.asList(theList)); - } - } - - /** - * Sets the paging provider to use, or null to use no paging (which is the default) - */ - public void setPagingProvider(IPagingProvider thePagingProvider) { - myPagingProvider = thePagingProvider; - } - - /** - * Sets the non-resource specific providers which implement method calls on this server. - * - * @see #setResourceProviders(Collection) - */ - public void setPlainProviders(Collection theProviders) { - myPlainProviders = theProviders; - } - - /** - * Sets the non-resource specific providers which implement method calls on this server. - * - * @see #setResourceProviders(Collection) - */ - public void setPlainProviders(Object... theProv) { - setPlainProviders(Arrays.asList(theProv)); - } - - /** - * Sets the non-resource specific providers which implement method calls on this server - * - * @see #setResourceProviders(Collection) - */ - public void setProviders(Object... theProviders) { - myPlainProviders = Arrays.asList(theProviders); - } - - /** - * Sets the resource providers for this server - */ - public void setResourceProviders(Collection theResourceProviders) { - myResourceProviders = theResourceProviders; - } - - /** - * Sets the resource providers for this server - */ - public void setResourceProviders(IResourceProvider... theResourceProviders) { - myResourceProviders = Arrays.asList(theResourceProviders); - } - - /** - * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy} - */ - public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { - Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); - myServerAddressStrategy = theServerAddressStrategy; - } - - /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement. - *

- * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null if you do not wish to export a conformance statement. - *

- * Note that this method can only be called before the server is initialized. - * - * @throws IllegalStateException - * Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that. - */ - public void setServerConformanceProvider(Object theServerConformanceProvider) { - if (myStarted) { - throw new IllegalStateException("Server is already started"); - } - myServerConformanceProvider = theServerConformanceProvider; - } - - /** - * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. - */ - public void setServerName(String theServerName) { - myServerName = theServerName; - } - - /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. - */ - public void setServerVersion(String theServerVersion) { - myServerVersion = theServerVersion; - } - - /** - * If set to true (default is false), the server will use browser friendly content-types (instead of standard FHIR ones) when it detects that the request is coming from a browser - * instead of a FHIR - */ - public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { - myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes; - } - - public void unregisterInterceptor(IServerInterceptor theInterceptor) { - Validate.notNull(theInterceptor, "Interceptor can not be null"); - myInterceptors.remove(theInterceptor); - } - - private static void addProfileToBundleEntry(FhirContext theContext, IResource theResource) { - - TagList tl = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); - if (tl == null) { - tl = new TagList(); - ResourceMetadataKeyEnum.TAG_LIST.put(theResource, tl); - } - - RuntimeResourceDefinition nextDef = theContext.getResourceDefinition(theResource); - String profile = nextDef.getResourceProfile(); - if (isNotBlank(profile)) { - tl.add(new Tag(Tag.HL7_ORG_PROFILE_TAG, profile, null)); - } - } - - public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding, - String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) { - theHttpResponse.setStatus(200); - - if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { - theHttpResponse.setContentType(theResponseEncoding.getBrowserFriendlyBundleContentType()); - } else if (theNarrativeMode == NarrativeModeEnum.ONLY) { - theHttpResponse.setContentType(Constants.CT_HTML); - } else { - theHttpResponse.setContentType(theResponseEncoding.getBundleContentType()); - } - - theHttpResponse.setCharacterEncoding(Constants.CHARSET_UTF_8); - - theServer.addHeadersToResponse(theHttpResponse); - - int numToReturn; - String searchId = null; - List resourceList; - if (theServer.getPagingProvider() == null) { - numToReturn = theResult.size(); - resourceList = theResult.getResources(0, numToReturn); - validateResourceListNotNull(resourceList); - - } else { - IPagingProvider pagingProvider = theServer.getPagingProvider(); - if (theLimit == null) { - numToReturn = pagingProvider.getDefaultPageSize(); - } else { - numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); - } - - numToReturn = Math.min(numToReturn, theResult.size() - theOffset); - resourceList = theResult.getResources(theOffset, numToReturn + theOffset); - validateResourceListNotNull(resourceList); - - if (theSearchId != null) { - searchId = theSearchId; - } else { - if (theResult.size() > numToReturn) { - searchId = pagingProvider.storeResultList(theResult); - Validate.notNull(searchId, "Paging provider returned null searchId"); - } - } - } - - for (IResource next : resourceList) { - if (next.getId() == null || next.getId().isEmpty()) { - if (!(next instanceof BaseOperationOutcome)) { - throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); - } - } - } - - if (theServer.getAddProfileTag() != AddProfileTagEnum.NEVER) { - for (int i = 0; i < resourceList.size(); i++) { - IResource nextRes = resourceList.get(i); - RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(nextRes); - if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardProfile()) { - addProfileToBundleEntry(theServer.getFhirContext(), nextRes); - } - } - } - - Bundle bundle = createBundleFromResourceList(theServer.getFhirContext(), theServer.getServerName(), resourceList, theServerBase, theCompleteUrl, theResult.size()); - - bundle.setPublished(theResult.getPublished()); - - if (theServer.getPagingProvider() != null) { - int limit; - limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); - limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); - - if (searchId != null) { - if (theOffset + numToReturn < theResult.size()) { - bundle.getLinkNext().setValue(createPagingLink(theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint)); - } - if (theOffset > 0) { - int start = Math.max(0, theOffset - limit); - bundle.getLinkPrevious().setValue(createPagingLink(theServerBase, searchId, start, limit, theResponseEncoding, thePrettyPrint)); - } - } - } - return bundle; - } - - private static void validateResourceListNotNull(List theResourceList) { - if (theResourceList == null) { - throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); - } - } - - public static Bundle createBundleFromResourceList(FhirContext theContext, String theAuthor, List theResult, String theServerBase, String theCompleteUrl, int theTotalResults) { - Bundle bundle = new Bundle(); - bundle.getAuthorName().setValue(theAuthor); - bundle.getBundleId().setValue(UUID.randomUUID().toString()); - bundle.getPublished().setToCurrentTimeInLocalTimeZone(); - bundle.getLinkBase().setValue(theServerBase); - bundle.getLinkSelf().setValue(theCompleteUrl); - - List addedResources = new ArrayList(); - Set addedResourceIds = new HashSet(); - for (IResource next : theResult) { - - Set containedIds = new HashSet(); - for (IResource nextContained : next.getContained().getContainedResources()) { - if (nextContained.getId().isEmpty() == false) { - containedIds.add(nextContained.getId().getValue()); - } - } - - if (theContext.getNarrativeGenerator() != null) { - String title = theContext.getNarrativeGenerator().generateTitle(next); - ourLog.trace("Narrative generator created title: {}", title); - if (StringUtils.isNotBlank(title)) { - ResourceMetadataKeyEnum.TITLE.put(next, title); - } - } else { - ourLog.trace("No narrative generator specified"); - } - - List references = theContext.newTerser().getAllPopulatedChildElementsOfType(next, ResourceReferenceDt.class); - do { - List addedResourcesThisPass = new ArrayList(); - - for (ResourceReferenceDt nextRef : references) { - IResource nextRes = nextRef.getResource(); - if (nextRes != null) { - if (nextRes.getId().hasIdPart()) { - if (containedIds.contains(nextRes.getId().getValue())) { - // Don't add contained IDs as top level resources - continue; - } - - IdDt id = nextRes.getId().toVersionless(); - if (id.hasResourceType() == false) { - String resName = theContext.getResourceDefinition(nextRes).getName(); - id = id.withResourceType(resName); - } - - if (!addedResourceIds.contains(id)) { - addedResourceIds.add(id); - addedResourcesThisPass.add(nextRes); - } - - nextRef.setResource(null); - nextRef.setReference(id); - } - } - } - - // Linked resources may themselves have linked resources - references = new ArrayList(); - for (IResource iResource : addedResourcesThisPass) { - List newReferences = theContext.newTerser().getAllPopulatedChildElementsOfType(iResource, ResourceReferenceDt.class); - references.addAll(newReferences); - } - - addedResources.addAll(addedResourcesThisPass); - - } while (references.isEmpty() == false); - - bundle.addResource(next, theContext, theServerBase); - - } - - /* - * Actually add the resources to the bundle - */ - for (IResource next : addedResources) { - bundle.addResource(next, theContext, theServerBase); - } - - bundle.getTotalResults().setValue(theTotalResults); - return bundle; - } - - public static String createPagingLink(String theServerBase, String theSearchId, int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint) { - StringBuilder b = new StringBuilder(); - b.append(theServerBase); - b.append('?'); - b.append(Constants.PARAM_PAGINGACTION); - b.append('='); - try { - b.append(URLEncoder.encode(theSearchId, "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new Error("UTF-8 not supported", e);// should not happen - } - b.append('&'); - b.append(Constants.PARAM_PAGINGOFFSET); - b.append('='); - b.append(theOffset); - b.append('&'); - b.append(Constants.PARAM_COUNT); - b.append('='); - b.append(theCount); - b.append('&'); - b.append(Constants.PARAM_FORMAT); - b.append('='); - b.append(theResponseEncoding.getRequestContentType()); - if (thePrettyPrint) { - b.append('&'); - b.append(Constants.PARAM_PRETTY); - b.append('='); - b.append(Constants.PARAM_PRETTY_VALUE_TRUE); - } - return b.toString(); - } - - public static NarrativeModeEnum determineNarrativeMode(RequestDetails theRequest) { - Map requestParams = theRequest.getParameters(); - String[] narrative = requestParams.remove(Constants.PARAM_NARRATIVE); - NarrativeModeEnum narrativeMode = null; - if (narrative != null && narrative.length > 0) { - narrativeMode = NarrativeModeEnum.valueOfCaseInsensitive(narrative[0]); - } - if (narrativeMode == null) { - narrativeMode = NarrativeModeEnum.NORMAL; - } - return narrativeMode; - } - - public static EncodingEnum determineRequestEncoding(Request theReq) { - Enumeration acceptValues = theReq.getServletRequest().getHeaders(Constants.HEADER_CONTENT_TYPE); - if (acceptValues != null) { - while (acceptValues.hasMoreElements()) { - String nextAcceptHeaderValue = acceptValues.nextElement(); - if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) { - for (String nextPart : nextAcceptHeaderValue.split(",")) { - int scIdx = nextPart.indexOf(';'); - if (scIdx == 0) { - continue; - } - if (scIdx != -1) { - nextPart = nextPart.substring(0, scIdx); - } - nextPart = nextPart.trim(); - EncodingEnum retVal = Constants.FORMAT_VAL_TO_ENCODING.get(nextPart); - if (retVal != null) { - return retVal; - } - } - } - } - } - return EncodingEnum.XML; - } - - /** -<<<<<<< HEAD - * Determine whether a response should be given in JSON or XML format based on the - * incoming HttpServletRequest's "_format" parameter and "Accept:" - * HTTP header. -======= - * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's "_format" parameter and "Accept:" HTTP header. ->>>>>>> d22a35788f57e9f7ce64bc8afc2ee7eaf29d94f2 - */ - public static EncodingEnum determineResponseEncoding(HttpServletRequest theReq) { - String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT); - if (format != null) { - for (String nextFormat : format) { - EncodingEnum retVal = Constants.FORMAT_VAL_TO_ENCODING.get(nextFormat); - if (retVal != null) { - return retVal; - } - } - } - - Enumeration acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT); - if (acceptValues != null) { - while (acceptValues.hasMoreElements()) { - String nextAcceptHeaderValue = acceptValues.nextElement(); - if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) { - for (String nextPart : nextAcceptHeaderValue.split(",")) { - int scIdx = nextPart.indexOf(';'); - if (scIdx == 0) { - continue; - } - if (scIdx != -1) { - nextPart = nextPart.substring(0, scIdx); - } - nextPart = nextPart.trim(); - EncodingEnum retVal = Constants.FORMAT_VAL_TO_ENCODING.get(nextPart); - if (retVal != null) { - return retVal; - } - } - } - } - } - return EncodingEnum.XML; - } - - public static Integer extractCountParameter(HttpServletRequest theRequest) { - String name = Constants.PARAM_COUNT; - return tryToExtractNamedParameter(theRequest, name); - } - - public static IParser getNewParser(FhirContext theContext, EncodingEnum theResponseEncoding, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode) { - IParser parser; - switch (theResponseEncoding) { - case JSON: - parser = theContext.newJsonParser(); - break; - case XML: - default: - parser = theContext.newXmlParser(); - break; - } - return parser.setPrettyPrint(thePrettyPrint).setSuppressNarratives(theNarrativeMode == NarrativeModeEnum.SUPPRESS); - } - - private static Writer getWriter(HttpServletResponse theHttpResponse, boolean theRespondGzip) throws UnsupportedEncodingException, IOException { - Writer writer; - if (theRespondGzip) { - theHttpResponse.addHeader(Constants.HEADER_CONTENT_ENCODING, Constants.ENCODING_GZIP); - writer = new OutputStreamWriter(new GZIPOutputStream(theHttpResponse.getOutputStream()), "UTF-8"); - } else { - writer = theHttpResponse.getWriter(); - } - return writer; - } - - public static boolean prettyPrintResponse(Request theRequest) { - Map requestParams = theRequest.getParameters(); - String[] pretty = requestParams.remove(Constants.PARAM_PRETTY); - boolean prettyPrint; - if (pretty != null && pretty.length > 0) { - if (Constants.PARAM_PRETTY_VALUE_TRUE.equals(pretty[0])) { - prettyPrint = true; - } else { - prettyPrint = false; - } - } else { - prettyPrint = false; - Enumeration acceptValues = theRequest.getServletRequest().getHeaders(Constants.HEADER_ACCEPT); - if (acceptValues != null) { - while (acceptValues.hasMoreElements()) { - String nextAcceptHeaderValue = acceptValues.nextElement(); - if (nextAcceptHeaderValue.contains("pretty=true")) { - prettyPrint = true; - } - } - } - } - return prettyPrint; - } - - public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase, - boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) throws IOException { - assert !theServerBase.endsWith("/"); - - Writer writer = getWriter(theHttpResponse, theRespondGzip); - try { - if (theNarrativeMode == NarrativeModeEnum.ONLY) { - for (IResource next : bundle.toListOfResources()) { - writer.append(next.getText().getDiv().getValueAsString()); - writer.append("
"); - } - } else { - RestfulServer.getNewParser(theServer.getFhirContext(), theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeBundleToWriter(bundle, writer); - } - } finally { - writer.close(); - } - } - - public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, - boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip, String theServerBase) throws IOException { - int stausCode = 200; - streamResponseAsResource(theServer, theHttpResponse, theResource, theResponseEncoding, thePrettyPrint, theRequestIsBrowser, theNarrativeMode, stausCode, theRespondGzip, theServerBase); - } - - private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, - boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode, boolean theRespondGzip, String theServerBase) throws IOException { - theHttpResponse.setStatus(stausCode); - - if (theResource.getId() != null && theResource.getId().hasIdPart() && isNotBlank(theServerBase)) { - String resName = theServer.getFhirContext().getResourceDefinition(theResource).getName(); - String fullId = theResource.getId().withServerBase(theServerBase, resName); - theHttpResponse.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId); - } - - if (theServer.getAddProfileTag() != AddProfileTagEnum.NEVER) { - RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(theResource); - if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardProfile()) { - addProfileToBundleEntry(theServer.getFhirContext(), theResource); - } - } - - if (theResource instanceof Binary) { - Binary bin = (Binary) theResource; - if (isNotBlank(bin.getContentType())) { - theHttpResponse.setContentType(bin.getContentType()); - } else { - theHttpResponse.setContentType(Constants.CT_OCTET_STREAM); - } - if (bin.getContent() == null || bin.getContent().length == 0) { - return; - } - - theHttpResponse.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); - - theHttpResponse.setContentLength(bin.getContent().length); - ServletOutputStream oos = theHttpResponse.getOutputStream(); - oos.write(bin.getContent()); - oos.close(); - return; - } - - if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { - theHttpResponse.setContentType(theResponseEncoding.getBrowserFriendlyBundleContentType()); - } else if (theNarrativeMode == NarrativeModeEnum.ONLY) { - theHttpResponse.setContentType(Constants.CT_HTML); - } else { - theHttpResponse.setContentType(theResponseEncoding.getResourceContentType()); - } - theHttpResponse.setCharacterEncoding(Constants.CHARSET_UTF_8); - - theServer.addHeadersToResponse(theHttpResponse); - - InstantDt lastUpdated = ResourceMetadataKeyEnum.UPDATED.get(theResource); - if (lastUpdated != null) { - theHttpResponse.addHeader(Constants.HEADER_LAST_MODIFIED, lastUpdated.getValueAsString()); - } - - TagList list = (TagList) theResource.getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST); - if (list != null) { - for (Tag tag : list) { - if (StringUtils.isNotBlank(tag.getTerm())) { - theHttpResponse.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue()); - } - } - } - - Writer writer = getWriter(theHttpResponse, theRespondGzip); - try { - if (theNarrativeMode == NarrativeModeEnum.ONLY) { - writer.append(theResource.getText().getDiv().getValueAsString()); - } else { - RestfulServer.getNewParser(theServer.getFhirContext(), theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeResourceToWriter(theResource, writer); - } - } finally { - writer.close(); - } - } - - private static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) { - String countString = theRequest.getParameter(name); - Integer count = null; - if (isNotBlank(countString)) { - try { - count = Integer.parseInt(countString); - } catch (NumberFormatException e) { - ourLog.debug("Failed to parse _count value '{}': {}", countString, e); - } - } - return count; - } - - public enum NarrativeModeEnum { - NORMAL, ONLY, SUPPRESS; - - public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { - return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); - } - } - -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.java index 4ac04970d8d..fa21d32f793 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.java @@ -42,12 +42,13 @@ public abstract class BaseServerResponseException extends RuntimeException { registerExceptionType(InternalErrorException.STATUS_CODE, InternalErrorException.class); registerExceptionType(InvalidRequestException.STATUS_CODE, InvalidRequestException.class); registerExceptionType(MethodNotAllowedException.STATUS_CODE, MethodNotAllowedException.class); + registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class); + registerExceptionType(NotModifiedException.STATUS_CODE, NotModifiedException.class); registerExceptionType(ResourceNotFoundException.STATUS_CODE, ResourceNotFoundException.class); - registerExceptionType(ResourceVersionNotSpecifiedException.STATUS_CODE, ResourceVersionNotSpecifiedException.class); + registerExceptionType(ResourceGoneException.STATUS_CODE, ResourceGoneException.class); + registerExceptionType(PreconditionFailedException.STATUS_CODE, PreconditionFailedException.class); registerExceptionType(ResourceVersionConflictException.STATUS_CODE, ResourceVersionConflictException.class); registerExceptionType(UnprocessableEntityException.STATUS_CODE, UnprocessableEntityException.class); - registerExceptionType(ResourceGoneException.STATUS_CODE, ResourceGoneException.class); - registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class); } private List myAdditionalMessages = null; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotImplementedOperationException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotImplementedOperationException.java index 0894a3d42ac..db807481ee2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotImplementedOperationException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotImplementedOperationException.java @@ -1,4 +1,5 @@ package ca.uhn.fhir.rest.server.exceptions; + import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.rest.server.Constants; @@ -22,37 +23,31 @@ import ca.uhn.fhir.rest.server.Constants; * #L% */ - /** - * Represents an HTTP 400 Bad Request response. - * This status indicates that the resource provider currently lacks the ability - * to fullfill the request. There is a good change that the functionality will - * be added in the future - * - * This Represents an HTTP 501 Not Implemented response, which means the resource provider currently lacks the ability to fullfill the request. - * + * This Represents an HTTP 501 Not Implemented response, which means the resource provider currently lacks the + * ability to fullfill the request. + * *

- * Note that a complete list of RESTful exceptions is available in the - * Package Summary. - *

- * - * + * Note that a complete list of RESTful exceptions is available in the Package + * Summary. + *

*/ public class NotImplementedOperationException extends BaseServerResponseException { -public static final int STATUS_CODE = Constants.STATUS_HTTP_501_NOT_IMPLEMENTED; - private static final long serialVersionUID = 1L; + public static final int STATUS_CODE = Constants.STATUS_HTTP_501_NOT_IMPLEMENTED; + private static final long serialVersionUID = 1L; + + public NotImplementedOperationException(String theMessage) { + super(STATUS_CODE, theMessage); + } - public NotImplementedOperationException(String theMessage) { - super(STATUS_CODE, theMessage); - } - /** * Constructor * * @param theMessage * The message - * @param theOperationOutcome The OperationOutcome resource to return to the client + * @param theOperationOutcome + * The OperationOutcome resource to return to the client */ public NotImplementedOperationException(String theMessage, BaseOperationOutcome theOperationOutcome) { super(STATUS_CODE, theMessage, theOperationOutcome); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotModifiedException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotModifiedException.java new file mode 100644 index 00000000000..eb9a0d99639 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/NotModifiedException.java @@ -0,0 +1,57 @@ +package ca.uhn.fhir.rest.server.exceptions; + +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; +import ca.uhn.fhir.rest.server.Constants; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** + * This Represents an HTTP 301 Not Modified response, which means the resource has not + * changed since the last version the client retrieved. This exception should only be used + * as a part of the ETag workflow. + * + *

+ * Note that a complete list of RESTful exceptions is available in the Package + * Summary. + *

+ */ +public class NotModifiedException extends BaseServerResponseException { + + public static final int STATUS_CODE = Constants.STATUS_HTTP_304_NOT_MODIFIED; + private static final long serialVersionUID = 1L; + + public NotModifiedException(String theMessage) { + super(STATUS_CODE, theMessage); + } + + /** + * Constructor + * + * @param theMessage + * The message + * @param theOperationOutcome + * The OperationOutcome resource to return to the client + */ + public NotModifiedException(String theMessage, BaseOperationOutcome theOperationOutcome) { + super(STATUS_CODE, theMessage, theOperationOutcome); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/PreconditionFailedException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/PreconditionFailedException.java new file mode 100644 index 00000000000..f2fc28bf454 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/PreconditionFailedException.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.rest.server.exceptions; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.server.Constants; + +/** + * Represents an HTTP 412 Precondition Failed response. This exception + * should be thrown for an {@link Update} operation if that operation requires a version to + * be specified in an HTTP header, and none was. + */ +@SuppressWarnings("deprecation") +public class PreconditionFailedException extends ResourceVersionNotSpecifiedException { + @SuppressWarnings("hiding") + public static final int STATUS_CODE = Constants.STATUS_HTTP_412_PRECONDITION_FAILED; + private static final long serialVersionUID = 1L; + + public PreconditionFailedException(String error) { + super(STATUS_CODE, error); + } + + /** + * Constructor + * + * @param theMessage + * The message + * @param theOperationOutcome The OperationOutcome resource to return to the client + */ + public PreconditionFailedException(String theMessage, BaseOperationOutcome theOperationOutcome) { + super(STATUS_CODE, theMessage, theOperationOutcome); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java index 32ceed2c6a7..12f196368ab 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java @@ -25,9 +25,8 @@ import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.server.Constants; /** - * Represents an HTTP 412 Precondition Failed response. This exception - * should be thrown for an {@link Update} operation if that operation requires a version to - * be specified in an HTTP header, and none was. + * @deprecated Use {@link PreconditionFailedException} instead - This exception is + * strangely named and will be removed at some point. */ public class ResourceVersionNotSpecifiedException extends BaseServerResponseException { public static final int STATUS_CODE = Constants.STATUS_HTTP_412_PRECONDITION_FAILED; @@ -48,4 +47,19 @@ public class ResourceVersionNotSpecifiedException extends BaseServerResponseExce super(STATUS_CODE, theMessage, theOperationOutcome); } + public ResourceVersionNotSpecifiedException(int theStatusCode, String error) { + super(theStatusCode, error); + } + + /** + * Constructor + * + * @param theMessage + * The message + * @param theOperationOutcome The OperationOutcome resource to return to the client + */ + public ResourceVersionNotSpecifiedException(int theStatusCode, String theMessage, BaseOperationOutcome theOperationOutcome) { + super(theStatusCode, theMessage, theOperationOutcome); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ICallable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ICallable.java new file mode 100644 index 00000000000..34307d00ab7 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ICallable.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.util; + +public interface ICallable { + + T call(); + +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index bf4786fb6af..4ef412c5b23 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.provider.JpaConformanceProvider; import ca.uhn.fhir.jpa.provider.JpaSystemProvider; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; +import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -57,6 +58,7 @@ public class TestRestfulServer extends RestfulServer { List beans; JpaSystemProvider systemProvider; IFhirSystemDao systemDao; + ETagSupportEnum etagSupport; switch (fhirVersionParam.trim().toUpperCase()) { case "DSTU": case "DSTU1": @@ -64,17 +66,21 @@ public class TestRestfulServer extends RestfulServer { beans = myAppCtx.getBean("myResourceProvidersDstu1", List.class); systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProvider.class); systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); + etagSupport = ETagSupportEnum.DISABLED; break; case "DEV": setFhirContext(FhirContext.forDev()); beans = myAppCtx.getBean("myResourceProvidersDev", List.class); systemProvider = myAppCtx.getBean("mySystemProviderDev", JpaSystemProvider.class); systemDao = myAppCtx.getBean("mySystemDaoDev", IFhirSystemDao.class); + etagSupport = ETagSupportEnum.ENABLED; break; default: throw new ServletException("Unknown FHIR version specified in init-param[FhirVersion]: " + fhirVersionParam); } + setETagSupport(etagSupport); + FhirContext ctx = getFhirContext(); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); @@ -107,18 +113,4 @@ public class TestRestfulServer extends RestfulServer { } - @Override - public void destroy() { - super.destroy(); - -// myAppCtx.close(); -// -// try { -// ourLog.info("Shutting down derby"); -// DriverManager.getConnection("jdbc:derby:directory:" + System.getProperty("fhir.db.location") + ";shutdown=true"); -// } catch (Exception e) { -// ourLog.info("Failed to create database: {}",e.getMessage()); -// } - } - } diff --git a/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/client/ETagClientTest.java b/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/client/ETagClientTest.java new file mode 100644 index 00000000000..557fca9a68c --- /dev/null +++ b/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/client/ETagClientTest.java @@ -0,0 +1,300 @@ +package ca.uhn.fhir.rest.client; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.ReaderInputStream; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.hamcrest.core.StringContains; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; + +import com.google.common.base.Preconditions; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.dev.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class ETagClientTest { + + private static FhirContext myCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientTest.class); + private HttpClient myHttpClient; + + private HttpResponse myHttpResponse; + + @Before + public void before() { + + myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + myCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + + myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + } + + private String extractBody(ArgumentCaptor capt, int count) throws IOException { + String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(count)).getEntity().getContent(), "UTF-8"); + return body; + } + + + private String getResourceResult() { + //@formatter:off + String msg = + "" + + "
John Cardinal: 444333333
" + + "" + + "" + + "" + + "" + + "" + + "
" + + "
"; + //@formatter:on + return msg; + } + + + private Patient getResource() { + return myCtx.newXmlParser().parseResource(Patient.class, getResourceResult()); + } + + @Test + public void testReadWithContentLocationInResponse() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + //@formatter:off + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; label=\"Some tag\""), + new BasicHeader(Constants.HEADER_ETAG, "\"9999\"") + }; + //@formatter:on + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client.read(Patient.class, new IdDt("Patient/1234")); + + assertEquals("http://foo.com/Patient/123/_history/2333", response.getId().getValue()); + + InstantDt lm = (InstantDt) response.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED); + lm.setTimeZoneZulu(true); + assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); + + TagList tags = ResourceMetadataKeyEnum.TAG_LIST.get(response); + assertNotNull(tags); + assertEquals(1, tags.size()); + assertEquals("http://foo/tagdefinition.html", tags.get(0).getTerm()); + assertEquals("http://hl7.org/fhir/tag", tags.get(0).getScheme()); + assertEquals("Some tag", tags.get(0).getLabel()); + } + + @Test + public void testReadWithIfNoneMatch() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_304_NOT_MODIFIED, "Not modified")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + try { + client + .read() + .resource(Patient.class) + .withId(new IdDt("Patient/1234")) + .execute(); + fail(); + } catch (NotModifiedException e) { + // good! + } + //@formatter:on + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + count++; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + Patient expected = new Patient(); + Patient response = client + .read() + .resource(Patient.class) + .withId(new IdDt("Patient/1234")) + .ifVersionMatches("9876").returnResource(expected) + .execute(); + //@formatter:on + assertSame(expected, response); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + assertEquals("\"9876\"", capt.getAllValues().get(count).getHeaders(Constants.HEADER_IF_NONE_MATCH_LC)[0].getValue()); + count++; + + } + + + @Test + public void testUpdateWithIfMatch() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_200_OK, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + client + .update() + .resource(getResource()) + .withId(new IdDt("Patient/1234")) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + assertEquals(0, capt.getAllValues().get(count).getHeaders(Constants.HEADER_IF_MATCH_LC).length); + count++; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + client + .update() + .resource(getResource()) + .withId(new IdDt("Patient/1234/_history/9876")) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + assertEquals("\"9876\"", capt.getAllValues().get(count).getHeaders(Constants.HEADER_IF_MATCH_LC)[0].getValue()); + count++; + + } + + + @Test + public void testUpdateWithIfMatchWithPreconditionFailed() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_412_PRECONDITION_FAILED, "Precondition Failed")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + try { + client + .update() + .resource(getResource()) + .withId(new IdDt("Patient/1234/_history/9876")) + .execute(); + fail(); + } catch (PreconditionFailedException e) { + // good + } + //@formatter:on + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + assertEquals("\"9876\"", capt.getAllValues().get(count).getHeaders(Constants.HEADER_IF_MATCH_LC)[0].getValue()); + count++; + + //@formatter:off + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""))); + try { + Patient resource = getResource(); + resource.setId(new IdDt("Patient/1234/_history/9876")); + client + .update() + .resource(resource) + .execute(); + fail(); + } catch (PreconditionFailedException e) { + // good + } + //@formatter:on + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count).getURI().toString()); + assertEquals("\"9876\"", capt.getAllValues().get(count).getHeaders(Constants.HEADER_IF_MATCH_LC)[0].getValue()); + count++; + } + + @Test + public void testReadWithETag() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; label=\"Some tag\"") }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + Patient response = client.read().resource(Patient.class).withId(new IdDt("Patient/1234")).execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId("1234").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); + + } + + + @BeforeClass + public static void beforeClass() { + myCtx = new FhirContext(); + } + +} diff --git a/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/server/ETagServerTest.java b/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/server/ETagServerTest.java new file mode 100644 index 00000000000..2423f7869cd --- /dev/null +++ b/hapi-fhir-structures-dev/src/test/java/ca/uhn/fhir/rest/server/ETagServerTest.java @@ -0,0 +1,219 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.dev.composite.IdentifierDt; +import ca.uhn.fhir.model.dev.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.util.PortUtil; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class ETagServerTest { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + private static Date ourLastModifiedDate; + private static int ourPort; + + private static Server ourServer; + private static PoolingHttpClientConnectionManager ourConnectionManager; + + @Test + public void testETagHeader() throws Exception { + ourLastModifiedDate = new InstantDt("2012-11-25T02:34:45.2222Z").getValue(); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/_history/3"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + IdentifierDt dt = ourCtx.newXmlParser().parseResource(Patient.class, responseContent).getIdentifierFirstRep(); + assertEquals("2", dt.getSystemElement().getValueAsString()); + assertEquals("3", dt.getValue()); + + Header cl = status.getFirstHeader(Constants.HEADER_ETAG_LC); + assertNotNull(cl); + assertEquals("\"222\"", cl.getValue()); + } + + + @Test + public void testAutomaticNotModified() throws Exception { + ourLastModifiedDate = new InstantDt("2012-11-25T02:34:45.2222Z").getValue(); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet.addHeader(Constants.HEADER_IF_NONE_MATCH, "\"222\""); + HttpResponse status = ourClient.execute(httpGet); + assertEquals(Constants.STATUS_HTTP_304_NOT_MODIFIED, status.getStatusLine().getStatusCode()); + } + + + @Test + public void testLastModifiedHeader() throws Exception { + ourLastModifiedDate = new InstantDt("2012-11-25T02:34:45.2222Z").getValue(); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/_history/3"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + IdentifierDt dt = ourCtx.newXmlParser().parseResource(Patient.class, responseContent).getIdentifierFirstRep(); + assertEquals("2", dt.getSystemElement().getValueAsString()); + assertEquals("3", dt.getValue()); + + Header cl = status.getFirstHeader(Constants.HEADER_LAST_MODIFIED_LOWERCASE); + assertNotNull(cl); + assertEquals("Sun, 25 Nov 2012 02:34:47 GMT", cl.getValue()); + } + + @Before + public void before() throws IOException { + ourLastId=null; + } + + @Test + public void testUpdateWithNoVersion() throws Exception { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("001"); + String resBody = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPut http; + http = new HttpPut("http://localhost:" + ourPort + "/Patient/2"); + http.setEntity(new StringEntity(resBody, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(http); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + + } + + @Test + public void testUpdateWithIfMatch() throws Exception { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("001"); + String resBody = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPut http; + http = new HttpPut("http://localhost:" + ourPort + "/Patient/2"); + http.setEntity(new StringEntity(resBody, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + http.addHeader(Constants.HEADER_IF_MATCH, "\"221\""); + CloseableHttpResponse status = ourClient.execute(http); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("Patient/2/_history/221", ourLastId.toUnqualified().getValue()); + + } + + @Test + public void testUpdateWithIfMatchPreconditionFailed() throws Exception { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("001"); + String resBody = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPut http; + http = new HttpPut("http://localhost:" + ourPort + "/Patient/2"); + http.setEntity(new StringEntity(resBody, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + http.addHeader(Constants.HEADER_IF_MATCH, "\"222\""); + CloseableHttpResponse status = ourClient.execute(http); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(Constants.STATUS_HTTP_412_PRECONDITION_FAILED, status.getStatusLine().getStatusCode()); + assertEquals("Patient/2/_history/222", ourLastId.toUnqualified().getValue()); + } + + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + PatientProvider patientProvider = new PatientProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(); + ourCtx = servlet.getFhirContext(); + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + ourConnectionManager = new PoolingHttpClientConnectionManager(50000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(ourConnectionManager); + ourClient = builder.build(); + + } + + private static IdDt ourLastId; + + public static class PatientProvider implements IResourceProvider { + + @Read(version = true) + public Patient findPatient(@IdParam IdDt theId) { + Patient patient = new Patient(); + ResourceMetadataKeyEnum.UPDATED.put(patient, new InstantDt(ourLastModifiedDate)); + patient.addIdentifier().setSystem(theId.getIdPart()).setValue(theId.getVersionIdPart()); + patient.setId(theId.withVersion("222")); + return patient; + } + + @Update + public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient theResource) { + ourLastId = theId; + + if ("222".equals(theId.getVersionIdPart())) { + throw new PreconditionFailedException("Bad version"); + } + + return new MethodOutcome(theId.withVersion(theId.getVersionIdPart() + "0")); + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + +} diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java index 4e567d5b225..222eebaaedf 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java @@ -384,6 +384,47 @@ public class GenericClientTest { } + @Test + public void testReadFluent() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; label=\"Some tag\"") }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + Patient response = client.read().resource(Patient.class).withId(new IdDt("Patient/1234")).execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId("1234").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); + assertThat(response.getNameFirstRep().getFamilyAsSingleString(), StringContains.containsString("Cardinal")); + assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); + + } + + @Test public void testReadWithAbsoluteUrl() throws Exception { diff --git a/src/changes/changes.xml b/src/changes/changes.xml index ae2160e7767..986e073850c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -23,6 +23,11 @@ parsed as a non-local reference. Thanks to Mohammad Jafari for reporting! + + Last-Modified]]> + header in server was incorrectly using FHIR date format instead + of RFC-1123 format. + diff --git a/src/site/site.xml b/src/site/site.xml index 165665dd701..d217626a159 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -76,6 +76,7 @@ + diff --git a/src/site/xdoc/doc_rest_client.xml b/src/site/xdoc/doc_rest_client.xml index 2190820dd1a..fd645fa897b 100644 --- a/src/site/xdoc/doc_rest_client.xml +++ b/src/site/xdoc/doc_rest_client.xml @@ -219,6 +219,14 @@ + +

+ See also the page on + ETag Support + for information on specifying a matching version in the + client request. +

+ @@ -248,6 +256,14 @@ + +

+ See also the page on + ETag Support + for information on specifying a matching version in the + client request. +

+
diff --git a/src/site/xdoc/doc_rest_etag.xml b/src/site/xdoc/doc_rest_etag.xml new file mode 100644 index 00000000000..15e77e5e9f0 --- /dev/null +++ b/src/site/xdoc/doc_rest_etag.xml @@ -0,0 +1,122 @@ + + + + + ETags - HAPI FHIR + James Agnew + + + + + +
+ + + + +

+ HAPI provides support for + HTTP ETags, which are + a standard way of providing faster reads when content has not changed and + optimistic locking for updates. +

+ +
+ +
+ +

+ ETag features are added simply by adding fluent method calls to the + client method chain, as shown in the following examples. +

+ + + + +

+ To notify the server that it should return an HTTP 301 Not Modified + if the content has not changed, add an ifVersionMatches(foo).[operation] + invocation. +

+ + + + + + +

+ This method will add the following header to the request: +

+ If-None-Match: "001" + +
+ +
+ + +

+ To implement version aware updates, specify a version in the + request. This will notify the server that it should only update the + resource on the server if the version matches the given version. This + is useful to prevent two clients from attempting to modify the + resource at the same time, and having one client's updates overwrite + the other's. +

+ + + + + + +

+ The following header will be added to the request as a part of this + interaction. +

+ If-Match: "001" + +
+ +
+ +
+ +

+ As of HAPI 0.9, ETag support is automatically enabled in the RESTful server. + This has the following effects: +

+
    +
  • + Read/VRead + method responses will include an + ETag header, noting the version + being returned. +
  • +
  • + If an incoming Read method includes an If-None-Match header with + the same version as the latest version being returned, the server will automatically + return an HTTP 304 Not Modified instead of returning the + resource body. +
  • +
+ + + +

+ To disable ETag support, simply invoke the + setETagSupport method, as in the following example. +

+ + + + + +
+ + + +
+ + + +