diff --git a/examples/src/main/java/example/ServerOperations.java b/examples/src/main/java/example/ServerOperations.java
index 069cc8a3ef4..c4ce7287e8c 100644
--- a/examples/src/main/java/example/ServerOperations.java
+++ b/examples/src/main/java/example/ServerOperations.java
@@ -1,7 +1,9 @@
package example;
+import java.io.IOException;
import java.util.List;
+import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.Parameters;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
@@ -18,11 +20,33 @@ import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
public class ServerOperations {
+ private static final Logger ourLog = LoggerFactory.getLogger(ServerOperations.class);
- //START SNIPPET: searchParamBasic
+
+ //START SNIPPET: manualInputAndOutput
+ @Operation(name="$manualInputAndOutput", manualResponse=true, manualRequest=true)
+ public void manualInputAndOutput(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws IOException {
+ String contentType = theServletRequest.getContentType();
+ byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream());
+
+ ourLog.info("Received call with content type {} and {} bytes", contentType, bytes.length);
+
+ theServletResponse.setContentType(contentType);
+ theServletResponse.getOutputStream().write(bytes);
+ theServletResponse.getOutputStream().close();
+ }
+ //END SNIPPET: manualInputAndOutput
+
+
+ //START SNIPPET: searchParamBasic
@Operation(name="$find-matches", idempotent=true)
public Parameters findMatchesBasic(
@OperationParam(name="date") DateParam theDate,
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/HookParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/HookParams.java
index 442a9630a97..16b496155cf 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/HookParams.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/HookParams.java
@@ -24,6 +24,8 @@ import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
import javax.annotation.Nonnull;
import java.util.Collection;
@@ -132,4 +134,11 @@ public class HookParams {
}
return this;
}
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SIMPLE_STYLE)
+ .append("params", myParams)
+ .toString();
+ }
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java
index 7eec865ab8a..ea8f21e734d 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java
@@ -25,6 +25,7 @@ import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@@ -131,4 +132,10 @@ public abstract class BasePrimitive extends BaseIdentifiableElement implement
public void writeExternal(ObjectOutput theOut) throws IOException {
theOut.writeObject(getValueAsString());
}
+
+ @Override
+ public boolean hasValue() {
+ return !StringUtils.isBlank(getValueAsString());
+ }
+
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IPrimitiveDatatypeWithPrecision.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IStreamingDatatype.java
similarity index 74%
rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IPrimitiveDatatypeWithPrecision.java
rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IStreamingDatatype.java
index 98b74766b70..8e2429eb6d1 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IPrimitiveDatatypeWithPrecision.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IStreamingDatatype.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.model.api;
-/*
+/*-
* #%L
* HAPI FHIR - Core Library
* %%
@@ -20,6 +20,13 @@ package ca.uhn.fhir.model.api;
* #L%
*/
-public interface IPrimitiveDatatypeWithPrecision extends IPrimitiveDatatype {
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
+
+import java.io.IOException;
+import java.io.Writer;
+
+public interface IStreamingDatatype extends IPrimitiveType {
+
+ void writeAsText(Writer theWriter) throws IOException;
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java
index f0bf9accc29..2ea9b162e5e 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java
@@ -87,4 +87,37 @@ public @interface Operation {
*/
BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION;
+ /**
+ * If this is set to true
(default is false
and this is almost
+ * always the right choice), the framework will not attempt to generate a response to
+ * this method.
+ *
+ * This is useful if you want to include an {@link javax.servlet.http.HttpServletResponse}
+ * in your method parameters and create a response yourself directly from your
+ * @Operation
method.
+ *
+ *
+ * Note that this will mean that interceptor methods will not get fired for the
+ * response, so there are security implications to using this flag.
+ *
+ */
+ boolean manualResponse() default false;
+
+ /**
+ * If this is set to true
(default is false
and this is almost
+ * always the right choice), the framework will not attempt to parse the request body,
+ * but will instead delegate it to the @Operation
method.
+ *
+ * This is useful if you want to include an {@link javax.servlet.http.HttpServletRequest}
+ * in your method parameters and parse the request yourself.
+ *
+ */
+ boolean manualRequest() default false;
+
+ /**
+ * If this is set to true
, this method will be a global operation
+ * meaning that it applies to all resource types
+ */
+ boolean global() default false;
+
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
index 72037f9e05e..28750936d87 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
@@ -211,6 +211,8 @@ public class Constants {
public static final String PARAMETER_CASCADE_DELETE = "_cascade";
public static final String HEADER_CASCADE = "X-Cascade";
public static final String CASCADE_DELETE = "delete";
+ public static final int MAX_RESOURCE_NAME_LENGTH = 100;
+ public static final String CACHE_CONTROL_PRIVATE = "private";
static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java
new file mode 100644
index 00000000000..9bedd26a7d6
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java
@@ -0,0 +1,98 @@
+package ca.uhn.fhir.util;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 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.context.BaseRuntimeChildDefinition;
+import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
+import ca.uhn.fhir.context.FhirContext;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.ICompositeType;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
+
+import java.util.List;
+
+public class AttachmentUtil {
+
+ /**
+ * Fetches the base64Binary value of Attachment.data, creating it if it does not
+ * already exist.
+ */
+ @SuppressWarnings("unchecked")
+ public static IPrimitiveType getOrCreateData(FhirContext theContext, ICompositeType theAttachment) {
+ BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "data");
+ List entries = entryChild.getAccessor().getValues(theAttachment);
+ return entries
+ .stream()
+ .map(t -> (IPrimitiveType) t)
+ .findFirst()
+ .orElseGet(() -> {
+ IPrimitiveType binary = newPrimitive(theContext, "base64Binary", null);
+ entryChild.getMutator().setValue(theAttachment, binary);
+ return binary;
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public static IPrimitiveType getOrCreateContentType(FhirContext theContext, ICompositeType theAttachment) {
+ BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "contentType");
+ List entries = entryChild.getAccessor().getValues(theAttachment);
+ return entries
+ .stream()
+ .map(t -> (IPrimitiveType) t)
+ .findFirst()
+ .orElseGet(() -> {
+ IPrimitiveType string = newPrimitive(theContext, "string", null);
+ entryChild.getMutator().setValue(theAttachment, string);
+ return string;
+ });
+ }
+
+ public static void setContentType(FhirContext theContext, ICompositeType theAttachment, String theContentType) {
+ BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "contentType");
+ entryChild.getMutator().setValue(theAttachment, newPrimitive(theContext, "code", theContentType));
+ }
+
+ public static void setData(FhirContext theContext, ICompositeType theAttachment, byte[] theBytes) {
+ BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "data");
+ entryChild.getMutator().setValue(theAttachment, newPrimitive(theContext, "base64Binary", theBytes));
+ }
+
+ public static void setSize(FhirContext theContext, ICompositeType theAttachment, Integer theLength) {
+ BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "size");
+ if (theLength == null) {
+ entryChild.getMutator().setValue(theAttachment, null);
+ } else {
+ entryChild.getMutator().setValue(theAttachment, newPrimitive(theContext, "unsignedInt", theLength));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static IPrimitiveType newPrimitive(FhirContext theContext, String theType, T theValue) {
+ IPrimitiveType primitive = (IPrimitiveType) theContext.getElementDefinition(theType).newInstance();
+ primitive.setValue(theValue);
+ return primitive;
+ }
+
+ private static BaseRuntimeChildDefinition getChild(FhirContext theContext, ICompositeType theAttachment, String theName) {
+ BaseRuntimeElementCompositeDefinition> def = (BaseRuntimeElementCompositeDefinition>) theContext.getElementDefinition(theAttachment.getClass());
+ return def.getChildByName(theName);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java
index b7a0c76d83f..2a466c1a788 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java
@@ -20,253 +20,148 @@ package ca.uhn.fhir.util;
* #L%
*/
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * .
- *
- */
-
import java.lang.ref.SoftReference;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
+import java.util.*;
/**
* A utility class for parsing and formatting HTTP dates as used in cookies and
* other headers. This class handles dates as defined by RFC 2616 section
* 3.3.1 as well as some other common non-standard formats.
+ *
+ * This class is basically intended to be a high-performance workaround
+ * for the fact that Java SimpleDateFormat is kind of expensive to
+ * create and yet isn't thread safe.
+ *
+ *
+ * This class was adapted from the class with the same name from the Jetty
+ * project, licensed under the terms of the Apache Software License 2.0.
+ *
*/
public final class DateUtils {
- /**
- * Date format pattern used to parse HTTP date headers in RFC 1123 format.
- */
- public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
+ public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1123 format.
+ */
+ private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1036 format.
+ */
+ private static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
+ /**
+ * Date format pattern used to parse HTTP date headers in ANSI C
+ * {@code asctime()} format.
+ */
+ private static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
+ private static final String[] DEFAULT_PATTERNS = new String[]{
+ PATTERN_RFC1123,
+ PATTERN_RFC1036,
+ PATTERN_ASCTIME
+ };
+ private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
- /**
- * Date format pattern used to parse HTTP date headers in RFC 1036 format.
- */
- public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
+ static {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeZone(GMT);
+ calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
+ }
- /**
- * Date format pattern used to parse HTTP date headers in ANSI C
- * {@code asctime()} format.
- */
- public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
+ /**
+ * This class should not be instantiated.
+ */
+ private DateUtils() {
+ }
- private static final String[] DEFAULT_PATTERNS = new String[] {
- PATTERN_RFC1123,
- PATTERN_RFC1036,
- PATTERN_ASCTIME
- };
+ /**
+ * A factory for {@link SimpleDateFormat}s. The instances are stored in a
+ * threadlocal way because SimpleDateFormat is not thread safe as noted in
+ * {@link SimpleDateFormat its javadoc}.
+ */
+ final static class DateFormatHolder {
- private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
+ private static final ThreadLocal>> THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(new HashMap<>()));
- public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+ /**
+ * creates a {@link SimpleDateFormat} for the requested format string.
+ *
+ * @param pattern a non-{@code null} format String according to
+ * {@link SimpleDateFormat}. The format is not checked against
+ * {@code null} since all paths go through
+ * {@link DateUtils}.
+ * @return the requested format. This simple DateFormat should not be used
+ * to {@link SimpleDateFormat#applyPattern(String) apply} to a
+ * different pattern.
+ */
+ static SimpleDateFormat formatFor(final String pattern) {
+ final SoftReference> ref = THREADLOCAL_FORMATS.get();
+ Map formats = ref.get();
+ if (formats == null) {
+ formats = new HashMap<>();
+ THREADLOCAL_FORMATS.set(
+ new SoftReference<>(formats));
+ }
- static {
- final Calendar calendar = Calendar.getInstance();
- calendar.setTimeZone(GMT);
- calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
- }
+ SimpleDateFormat format = formats.get(pattern);
+ if (format == null) {
+ format = new SimpleDateFormat(pattern, Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("GMT"));
+ formats.put(pattern, format);
+ }
- /**
- * Parses a date value. The formats used for parsing the date value are retrieved from
- * the default http params.
- *
- * @param dateValue the date value to parse
- *
- * @return the parsed date or null if input could not be parsed
- */
- public static Date parseDate(final String dateValue) {
- return parseDate(dateValue, null, null);
- }
+ return format;
+ }
- /**
- * Parses the date value using the given date formats.
- *
- * @param dateValue the date value to parse
- * @param dateFormats the date formats to use
- *
- * @return the parsed date or null if input could not be parsed
- */
- public static Date parseDate(final String dateValue, final String[] dateFormats) {
- return parseDate(dateValue, dateFormats, null);
- }
+ }
- /**
- * Parses the date value using the given date formats.
- *
- * @param dateValue the date value to parse
- * @param dateFormats the date formats to use
- * @param startDate During parsing, two digit years will be placed in the range
- * {@code startDate} to {@code startDate + 100 years}. This value may
- * be {@code null}. When {@code null} is given as a parameter, year
- * {@code 2000} will be used.
- *
- * @return the parsed date or null if input could not be parsed
- */
- public static Date parseDate(
- final String dateValue,
- final String[] dateFormats,
- final Date startDate) {
- notNull(dateValue, "Date value");
- final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS;
- final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START;
- String v = dateValue;
- // trim single quotes around date if present
- // see issue #5279
- if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
- v = v.substring (1, v.length() - 1);
- }
+ /**
+ * Parses a date value. The formats used for parsing the date value are retrieved from
+ * the default http params.
+ *
+ * @param theDateValue the date value to parse
+ * @return the parsed date or null if input could not be parsed
+ */
+ public static Date parseDate(final String theDateValue) {
+ notNull(theDateValue, "Date value");
+ String v = theDateValue;
+ if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
+ v = v.substring(1, v.length() - 1);
+ }
- for (final String dateFormat : localDateFormats) {
- final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
- dateParser.set2DigitYearStart(localStartDate);
- final ParsePosition pos = new ParsePosition(0);
- final Date result = dateParser.parse(v, pos);
- if (pos.getIndex() != 0) {
- return result;
- }
- }
- return null;
- }
+ for (final String dateFormat : DEFAULT_PATTERNS) {
+ final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
+ dateParser.set2DigitYearStart(DEFAULT_TWO_DIGIT_YEAR_START);
+ final ParsePosition pos = new ParsePosition(0);
+ final Date result = dateParser.parse(v, pos);
+ if (pos.getIndex() != 0) {
+ return result;
+ }
+ }
+ return null;
+ }
- /**
- * Formats the given date according to the RFC 1123 pattern.
- *
- * @param date The date to format.
- * @return An RFC 1123 formatted date string.
- *
- * @see #PATTERN_RFC1123
- */
- public static String formatDate(final Date date) {
- return formatDate(date, PATTERN_RFC1123);
- }
+ /**
+ * Formats the given date according to the RFC 1123 pattern.
+ *
+ * @param date The date to format.
+ * @return An RFC 1123 formatted date string.
+ * @see #PATTERN_RFC1123
+ */
+ public static String formatDate(final Date date) {
+ notNull(date, "Date");
+ notNull(PATTERN_RFC1123, "Pattern");
+ final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1123);
+ return formatter.format(date);
+ }
- /**
- * Formats the given date according to the specified pattern. The pattern
- * must conform to that used by the {@link SimpleDateFormat simple date
- * format} class.
- *
- * @param date The date to format.
- * @param pattern The pattern to use for formatting the date.
- * @return A formatted date string.
- *
- * @throws IllegalArgumentException If the given date pattern is invalid.
- *
- * @see SimpleDateFormat
- */
- public static String formatDate(final Date date, final String pattern) {
- notNull(date, "Date");
- notNull(pattern, "Pattern");
- final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
- return formatter.format(date);
- }
-
-
- public static T notNull(final T argument, final String name) {
- if (argument == null) {
- throw new IllegalArgumentException(name + " may not be null");
- }
- return argument;
- }
-
- /**
- * Clears thread-local variable containing {@link java.text.DateFormat} cache.
- *
- * @since 4.3
- */
- public static void clearThreadLocal() {
- DateFormatHolder.clearThreadLocal();
- }
-
- /** This class should not be instantiated. */
- private DateUtils() {
- }
-
- /**
- * A factory for {@link SimpleDateFormat}s. The instances are stored in a
- * threadlocal way because SimpleDateFormat is not threadsafe as noted in
- * {@link SimpleDateFormat its javadoc}.
- *
- */
- final static class DateFormatHolder {
-
- private static final ThreadLocal>>
- THREADLOCAL_FORMATS = new ThreadLocal>>() {
-
- @Override
- protected SoftReference> initialValue() {
- return new SoftReference>(
- new HashMap());
- }
-
- };
-
- /**
- * creates a {@link SimpleDateFormat} for the requested format string.
- *
- * @param pattern
- * a non-{@code null} format String according to
- * {@link SimpleDateFormat}. The format is not checked against
- * {@code null} since all paths go through
- * {@link DateUtils}.
- * @return the requested format. This simple dateformat should not be used
- * to {@link SimpleDateFormat#applyPattern(String) apply} to a
- * different pattern.
- */
- public static SimpleDateFormat formatFor(final String pattern) {
- final SoftReference> ref = THREADLOCAL_FORMATS.get();
- Map formats = ref.get();
- if (formats == null) {
- formats = new HashMap();
- THREADLOCAL_FORMATS.set(
- new SoftReference>(formats));
- }
-
- SimpleDateFormat format = formats.get(pattern);
- if (format == null) {
- format = new SimpleDateFormat(pattern, Locale.US);
- format.setTimeZone(TimeZone.getTimeZone("GMT"));
- formats.put(pattern, format);
- }
-
- return format;
- }
-
- public static void clearThreadLocal() {
- THREADLOCAL_FORMATS.remove();
- }
-
- }
+ public static T notNull(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException(name + " may not be null");
+ }
+ return argument;
+ }
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java
index 4d66a3d0b9d..727d27793b4 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java
@@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.escape.Escaper;
import com.google.common.net.PercentEscaper;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
@@ -44,6 +45,44 @@ public class UrlUtil {
private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
+ public static class UrlParts {
+ private String myParams;
+ private String myResourceId;
+ private String myResourceType;
+ private String myVersionId;
+
+ public String getParams() {
+ return myParams;
+ }
+
+ public void setParams(String theParams) {
+ myParams = theParams;
+ }
+
+ public String getResourceId() {
+ return myResourceId;
+ }
+
+ public void setResourceId(String theResourceId) {
+ myResourceId = theResourceId;
+ }
+
+ public String getResourceType() {
+ return myResourceType;
+ }
+
+ public void setResourceType(String theResourceType) {
+ myResourceType = theResourceType;
+ }
+
+ public String getVersionId() {
+ return myVersionId;
+ }
+
+ public void setVersionId(String theVersionId) {
+ myVersionId = theVersionId;
+ }
+ }
/**
* Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
@@ -115,7 +154,6 @@ public class UrlUtil {
return PARAMETER_ESCAPER.escape(theUnescaped);
}
-
public static boolean isAbsolute(String theValue) {
String value = theValue.toLowerCase();
return value.startsWith("http://") || value.startsWith("https://");
@@ -284,7 +322,7 @@ public class UrlUtil {
}
if (nextChar == '?') {
if (url.length() > idx + 1) {
- retVal.setParams(url.substring(idx + 1, url.length()));
+ retVal.setParams(url.substring(idx + 1));
}
break;
}
@@ -296,6 +334,18 @@ public class UrlUtil {
}
+ /**
+ * This method specifically HTML-encodes the " and
+ * < characters in order to prevent injection attacks
+ */
+ public static String sanitizeUrlPart(IPrimitiveType> theString) {
+ String retVal = null;
+ if (theString != null) {
+ retVal = sanitizeUrlPart(theString.getValueAsString());
+ }
+ return retVal;
+ }
+
/**
* This method specifically HTML-encodes the " and
* < characters in order to prevent injection attacks
@@ -352,6 +402,8 @@ public class UrlUtil {
char nextChar = theString.charAt(i);
if (nextChar == '%' || nextChar == '+') {
try {
+ // Yes it would be nice to not use a string "UTF-8" but the equivalent
+ // method that takes Charset is JDK10+ only... sigh....
return URLDecoder.decode(theString, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error("UTF-8 not supported, this shouldn't happen", e);
@@ -361,43 +413,4 @@ public class UrlUtil {
return theString;
}
- public static class UrlParts {
- private String myParams;
- private String myResourceId;
- private String myResourceType;
- private String myVersionId;
-
- public String getParams() {
- return myParams;
- }
-
- public void setParams(String theParams) {
- myParams = theParams;
- }
-
- public String getResourceId() {
- return myResourceId;
- }
-
- public void setResourceId(String theResourceId) {
- myResourceId = theResourceId;
- }
-
- public String getResourceType() {
- return myResourceType;
- }
-
- public void setResourceType(String theResourceType) {
- myResourceType = theResourceType;
- }
-
- public String getVersionId() {
- return myVersionId;
- }
-
- public void setVersionId(String theVersionId) {
- myVersionId = theVersionId;
- }
- }
-
}
diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java
index 746c249c896..52e032d587f 100644
--- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java
+++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java
@@ -29,6 +29,8 @@ public interface IPrimitiveType extends IBaseDatatype {
String getValueAsString();
T getValue();
+
+ boolean hasValue();
IPrimitiveType setValue(T theValue) throws IllegalArgumentException;
diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties
index 45d5426bda2..6c8b8758f71 100644
--- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties
+++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties
@@ -114,6 +114,8 @@ ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor.successMsg=Cascaded delet
ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor.noParam=Note that cascading deletes are not active for this request. You can enable cascading deletes by using the "_cascade=true" URL parameter.
ca.uhn.fhir.jpa.provider.BaseJpaProvider.cantCombintAtAndSince=Unable to combine _at and _since parameters for history operation
+ca.uhn.fhir.jpa.provider.BinaryAccessProvider.noAttachmentDataPresent=The resource with ID {0} has no data at path: {1}
+ca.uhn.fhir.jpa.provider.BinaryAccessProvider.unknownBlobId=Can not find the requested binary content. It may have been deleted.
ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl.cannotCreateDuplicateCodeSystemUrl=Can not create multiple CodeSystem resources with CodeSystem.url "{0}", already have one with resource ID: {1}
ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl.cannotCreateDuplicateConceptMapUrl=Can not create multiple ConceptMap resources with ConceptMap.url "{0}", already have one with resource ID: {1}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java
new file mode 100644
index 00000000000..41792fe7d70
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java
@@ -0,0 +1,66 @@
+package ca.uhn.fhir.jpa.binstore;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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 com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+import com.google.common.hash.HashingInputStream;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import javax.annotation.Nonnull;
+import java.io.InputStream;
+import java.security.SecureRandom;
+
+abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
+
+ private final SecureRandom myRandom;
+ private final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ private final int ID_LENGTH = 100;
+ private int myMinSize;
+
+ BaseBinaryStorageSvcImpl() {
+ myRandom = new SecureRandom();
+ }
+
+ public void setMinSize(int theMinSize) {
+ myMinSize = theMinSize;
+ }
+
+ String newRandomId() {
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < ID_LENGTH; i++) {
+ int nextInt = Math.abs(myRandom.nextInt());
+ b.append(CHARS.charAt(nextInt % CHARS.length()));
+ }
+ return b.toString();
+ }
+
+ @Override
+ public boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType) {
+ return theSize >= myMinSize;
+ }
+
+ @Nonnull
+ static HashingInputStream createHashingInputStream(InputStream theInputStream) {
+ HashFunction hash = Hashing.sha256();
+ return new HashingInputStream(hash, theInputStream);
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImpl.java
new file mode 100644
index 00000000000..addd99ab493
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImpl.java
@@ -0,0 +1,166 @@
+package ca.uhn.fhir.jpa.binstore;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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.context.ConfigurationException;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.google.common.base.Charsets;
+import com.google.common.hash.HashingInputStream;
+import com.google.common.io.CountingInputStream;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.Validate;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import java.io.*;
+import java.util.Date;
+
+public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
+
+ private static final Logger ourLog = LoggerFactory.getLogger(FilesystemBinaryStorageSvcImpl.class);
+ private final File myBasePath;
+ private final ObjectMapper myJsonSerializer;
+
+ public FilesystemBinaryStorageSvcImpl(String theBasePath) {
+ Validate.notBlank(theBasePath);
+
+ myBasePath = new File(theBasePath);
+
+ myJsonSerializer = new ObjectMapper();
+ myJsonSerializer.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ myJsonSerializer.enable(SerializationFeature.INDENT_OUTPUT);
+ }
+
+ @PostConstruct
+ public void start() {
+ ourLog.info("Starting binary storage service with base path: {}", myBasePath);
+
+ mkdir(myBasePath);
+ }
+
+ @Override
+ public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException {
+ String id = newRandomId();
+ File storagePath = getStoragePath(id, true);
+
+ // Write binary file
+ File storageFilename = getStorageFilename(storagePath, theResourceId, id);
+ ourLog.info("Writing to file: {}", storageFilename.getAbsolutePath());
+ CountingInputStream countingInputStream = new CountingInputStream(theInputStream);
+ HashingInputStream hashingInputStream = createHashingInputStream(countingInputStream);
+ try (FileOutputStream outputStream = new FileOutputStream(storageFilename)) {
+ IOUtils.copy(hashingInputStream, outputStream);
+ }
+
+ // Write descriptor file
+ long count = countingInputStream.getCount();
+ StoredDetails details = new StoredDetails(id, count, theContentType, hashingInputStream, new Date());
+ File descriptorFilename = getDescriptorFilename(storagePath, theResourceId, id);
+ ourLog.info("Writing to file: {}", descriptorFilename.getAbsolutePath());
+ try (FileWriter writer = new FileWriter(descriptorFilename)) {
+ myJsonSerializer.writeValue(writer, details);
+ }
+
+ ourLog.info("Stored binary blob with {} bytes and ContentType {} for resource {}", count, theContentType, theResourceId);
+
+ return details;
+ }
+
+ @Override
+ public StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) throws IOException {
+ StoredDetails retVal = null;
+
+ File storagePath = getStoragePath(theBlobId, false);
+ if (storagePath != null) {
+ File file = getDescriptorFilename(storagePath, theResourceId, theBlobId);
+ if (file.exists()) {
+ try (InputStream inputStream = new FileInputStream(file)) {
+ try (Reader reader = new InputStreamReader(inputStream, Charsets.UTF_8)) {
+ retVal = myJsonSerializer.readValue(reader, StoredDetails.class);
+ }
+ }
+ }
+ }
+
+ return retVal;
+ }
+
+ @Override
+ public void writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException {
+ File storagePath = getStoragePath(theBlobId, false);
+ if (storagePath != null) {
+ File file = getStorageFilename(storagePath, theResourceId, theBlobId);
+ if (file.exists()) {
+ try (InputStream inputStream = new FileInputStream(file)) {
+ IOUtils.copy(inputStream, theOutputStream);
+ theOutputStream.close();
+ }
+ }
+ }
+ }
+
+ @Nonnull
+ private File getDescriptorFilename(File theStoragePath, IIdType theResourceId, String theId) {
+ return getStorageFilename(theStoragePath, theResourceId, theId, ".json");
+ }
+
+ @Nonnull
+ private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId) {
+ return getStorageFilename(theStoragePath, theResourceId, theId, ".bin");
+ }
+
+ private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId, String theExtension) {
+ Validate.notBlank(theResourceId.getResourceType());
+ Validate.notBlank(theResourceId.getIdPart());
+
+ String filename = theResourceId.getResourceType() + "_" + theResourceId.getIdPart() + "_" + theId;
+ return new File(theStoragePath, filename + theExtension);
+ }
+
+ private File getStoragePath(String theId, boolean theCreate) {
+ File path = myBasePath;
+ for (int i = 0; i < 10; i++) {
+ path = new File(path, theId.substring(i, i+1));
+ if (!path.exists()) {
+ if (theCreate) {
+ mkdir(path);
+ } else {
+ return null;
+ }
+ }
+ }
+ return path;
+ }
+
+ private void mkdir(File theBasePath) {
+ try {
+ FileUtils.forceMkdir(theBasePath);
+ } catch (IOException e) {
+ throw new ConfigurationException("Unable to create path " + myBasePath + ": " + e.toString());
+ }
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java
new file mode 100644
index 00000000000..d37ab6b42bb
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java
@@ -0,0 +1,137 @@
+package ca.uhn.fhir.jpa.binstore;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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.jpa.util.JsonDateDeserializer;
+import ca.uhn.fhir.jpa.util.JsonDateSerializer;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.google.common.hash.HashingInputStream;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+
+public interface IBinaryStorageSvc {
+
+ /**
+ * Give the storage service the ability to veto items from storage
+ *
+ * @param theSize How large is the item
+ * @param theResourceId What is the resource ID it will be associated with
+ * @param theContentType What is the content type
+ * @return true
if the storage service should store the item
+ */
+ boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType);
+
+ /**
+ * Store a new binary blob
+ *
+ * @param theResourceId The resource ID that owns this blob. Note that it should not be possible to retrieve a blob without both the resource ID and the blob ID being correct.
+ * @param theContentType The content type to associate with this blob
+ * @param theInputStream An InputStream to read from. This method should close the stream when it has been fully consumed.
+ * @return Returns details about the stored data
+ */
+ StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException;
+
+ StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) throws IOException;
+
+ void writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException;
+
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
+ class StoredDetails {
+
+ @JsonProperty("blobId")
+ private String myBlobId;
+ @JsonProperty("bytes")
+ private long myBytes;
+ @JsonProperty("contentType")
+ private String myContentType;
+ @JsonProperty("hash")
+ private String myHash;
+ @JsonProperty("published")
+ @JsonSerialize(using = JsonDateSerializer.class)
+ @JsonDeserialize(using = JsonDateDeserializer.class)
+ private Date myPublished;
+
+ /**
+ * Constructor
+ */
+ @SuppressWarnings("unused")
+ public StoredDetails() {
+ super();
+ }
+
+ /**
+ * Constructor
+ */
+ public StoredDetails(@Nonnull String theBlobId, long theBytes, @Nonnull String theContentType, HashingInputStream theIs, Date thePublished) {
+ myBlobId = theBlobId;
+ myBytes = theBytes;
+ myContentType = theContentType;
+ myHash = theIs.hash().toString();
+ myPublished = thePublished;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("blobId", myBlobId)
+ .append("bytes", myBytes)
+ .append("contentType", myContentType)
+ .append("hash", myHash)
+ .append("published", myPublished)
+ .toString();
+ }
+
+ public String getHash() {
+ return myHash;
+ }
+
+ public Date getPublished() {
+ return myPublished;
+ }
+
+ @Nonnull
+ public String getContentType() {
+ return myContentType;
+ }
+
+ @Nonnull
+ public String getBlobId() {
+ return myBlobId;
+ }
+
+ public long getBytes() {
+ return myBytes;
+ }
+
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/MemoryBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/MemoryBinaryStorageSvcImpl.java
new file mode 100644
index 00000000000..2774c36063e
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/MemoryBinaryStorageSvcImpl.java
@@ -0,0 +1,82 @@
+package ca.uhn.fhir.jpa.binstore;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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 com.google.common.hash.HashingInputStream;
+import org.apache.commons.io.IOUtils;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Purely in-memory implementation of binary storage service. This is really
+ * only appropriate for testing, since it doesn't persist anywhere and is
+ * limited by the amount of available RAM.
+ */
+public class MemoryBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
+
+ private ConcurrentHashMap myDataMap = new ConcurrentHashMap<>();
+ private ConcurrentHashMap myDetailsMap = new ConcurrentHashMap<>();
+
+ /**
+ * Constructor
+ */
+ public MemoryBinaryStorageSvcImpl() {
+ super();
+ }
+
+ @Override
+ public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException {
+ String id = newRandomId();
+ String key = toKey(theResourceId, id);
+
+ HashingInputStream is = createHashingInputStream(theInputStream);
+
+ byte[] bytes = IOUtils.toByteArray(is);
+ theInputStream.close();
+ myDataMap.put(key, bytes);
+ StoredDetails storedDetails = new StoredDetails(id, bytes.length, theContentType, is, new Date());
+ myDetailsMap.put(key, storedDetails);
+ return storedDetails;
+ }
+
+ @Override
+ public StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) {
+ String key = toKey(theResourceId, theBlobId);
+ return myDetailsMap.get(key);
+ }
+
+ @Override
+ public void writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException {
+ String key = toKey(theResourceId, theBlobId);
+ byte[] bytes = myDataMap.get(key);
+ theOutputStream.write(bytes);
+ }
+
+ private String toKey(IIdType theResourceId, String theBlobId) {
+ return theBlobId + '-' + theResourceId.toUnqualifiedVersionless().getValue();
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java
new file mode 100644
index 00000000000..a048fdd2496
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java
@@ -0,0 +1,31 @@
+package ca.uhn.fhir.jpa.binstore;
+
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class NullBinaryStorageSvcImpl implements IBinaryStorageSvc {
+
+ @Override
+ public boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType) {
+ return false;
+ }
+
+ @Override
+ public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
index cda11b29aee..ac2c94ddb8c 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
@@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
+import ca.uhn.fhir.jpa.provider.BinaryAccessProvider;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@@ -120,6 +121,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new SubscriptionTriggeringProvider();
}
+ @Bean(name = "myAttachmentBinaryAccessProvider")
+ @Lazy
+ public BinaryAccessProvider AttachmentBinaryAccessProvider() {
+ return new BinaryAccessProvider();
+ }
+
@Bean
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
index 7d214d65e5d..dd76b67e210 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
@@ -25,7 +25,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
index ab880ed8f65..e0e874c1aad 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
@@ -561,7 +561,7 @@ public abstract class BaseHapiFhirResourceDao extends B
return myExpungeService.expunge(getResourceName(), entity.getResourceId(), entity.getVersion(), theExpungeOptions, theRequest);
}
- return myExpungeService.expunge(getResourceName(), entity.getResourceId(), null, theExpungeOptions ,theRequest);
+ return myExpungeService.expunge(getResourceName(), entity.getResourceId(), null, theExpungeOptions, theRequest);
}
@Override
@@ -851,6 +851,7 @@ public abstract class BaseHapiFhirResourceDao extends B
}
}
}
+
}
@Override
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java
index 2dc0fc60904..5a06a127537 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java
@@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity;
* #L%
*/
+import ca.uhn.fhir.rest.api.Constants;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -36,7 +37,7 @@ public class ResourceReindexJobEntity implements Serializable {
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RES_REINDEX_JOB")
@Column(name = "PID")
private Long myId;
- @Column(name = "RES_TYPE", nullable = true)
+ @Column(name = "RES_TYPE", nullable = true, length = Constants.MAX_RESOURCE_NAME_LENGTH)
private String myResourceType;
/**
* Inclusive
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
index a677cec0221..60515aaa4bf 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
@@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.entity;
*/
import ca.uhn.fhir.context.FhirVersionEnum;
+import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.model.primitive.IdDt;
@@ -47,7 +48,7 @@ import java.util.Date;
", h.res_updated as res_updated " +
", h.res_text as res_text " +
", h.res_encoding as res_encoding " +
- ", f.forced_id as forced_pid " +
+ ", f.forced_id as FORCED_PID " +
"FROM HFJ_RES_VER h "
+ " LEFT OUTER JOIN HFJ_FORCED_ID f ON f.resource_pid = h.res_id "
+ " INNER JOIN HFJ_RESOURCE r ON r.res_id = h.res_id and r.res_ver = h.res_ver")
@@ -63,7 +64,7 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable {
@Column(name = "RES_ID")
private Long myResourceId;
- @Column(name = "RES_TYPE")
+ @Column(name = "RES_TYPE", length = Constants.MAX_RESOURCE_NAME_LENGTH)
private String myResourceType;
@Column(name = "RES_VERSION")
@@ -96,7 +97,7 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable {
@Enumerated(EnumType.STRING)
private ResourceEncodingEnum myEncoding;
- @Column(name = "forced_pid")
+ @Column(name = "FORCED_PID", length= ForcedId.MAX_FORCED_ID_LENGTH)
private String myForcedPid;
public ResourceSearchView() {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java
index 60fc0d1a8e4..5249be71b4f 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java
@@ -44,7 +44,7 @@ import static org.apache.commons.lang3.StringUtils.length;
@Entity
@Indexed(interceptor = DeferConceptIndexingInterceptor.class)
@Table(name = "TRM_CONCEPT", uniqueConstraints = {
- @UniqueConstraint(name = "IDX_CONCEPT_CS_CODE", columnNames = {"CODESYSTEM_PID", "CODE"})
+ @UniqueConstraint(name = "IDX_CONCEPT_CS_CODE", columnNames = {"CODESYSTEM_PID", "CODEVAL"})
}, indexes = {
@Index(name = "IDX_CONCEPT_INDEXSTATUS", columnList = "INDEX_STATUS"),
@Index(name = "IDX_CONCEPT_UPDATED", columnList = "CONCEPT_UPDATED")
@@ -60,7 +60,7 @@ public class TermConcept implements Serializable {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "myParent", cascade = {})
private Collection myChildren;
- @Column(name = "CODE", nullable = false, length = MAX_CODE_LENGTH)
+ @Column(name = "CODEVAL", nullable = false, length = MAX_CODE_LENGTH)
@Fields({@Field(name = "myCode", index = org.hibernate.search.annotations.Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "exactAnalyzer")),})
private String myCode;
@Temporal(TemporalType.TIMESTAMP)
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSet.java
index f586a9df8db..723472895bc 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSet.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSet.java
@@ -62,7 +62,7 @@ public class TermValueSet implements Serializable {
@Column(name = "RES_ID", insertable = false, updatable = false)
private Long myResourcePid;
- @Column(name = "NAME", nullable = true, length = MAX_NAME_LENGTH)
+ @Column(name = "VSNAME", nullable = true, length = MAX_NAME_LENGTH)
private String myName;
@OneToMany(mappedBy = "myValueSet")
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java
index 49de4907693..078bedb8bda 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java
@@ -36,7 +36,7 @@ import static org.apache.commons.lang3.StringUtils.left;
import static org.apache.commons.lang3.StringUtils.length;
@Table(name = "TRM_VALUESET_CONCEPT", indexes = {
- @Index(name = "IDX_VALUESET_CONCEPT_CS_CD", columnList = "SYSTEM, CODE")
+ @Index(name = "IDX_VALUESET_CONCEPT_CS_CD", columnList = "SYSTEM, CODEVAL")
})
@Entity()
public class TermValueSetConcept implements Serializable {
@@ -58,10 +58,10 @@ public class TermValueSetConcept implements Serializable {
@Transient
private String myValueSetName;
- @Column(name = "SYSTEM", nullable = false, length = TermCodeSystem.MAX_URL_LENGTH)
+ @Column(name = "SYSTEM_URL", nullable = false, length = TermCodeSystem.MAX_URL_LENGTH)
private String mySystem;
- @Column(name = "CODE", nullable = false, length = TermConcept.MAX_CODE_LENGTH)
+ @Column(name = "CODEVAL", nullable = false, length = TermConcept.MAX_CODE_LENGTH)
private String myCode;
@Column(name = "DISPLAY", nullable = true, length = TermConcept.MAX_DESC_LENGTH)
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java
index 6c8a6fb71c2..e0ce7b36245 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java
@@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderCompositionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderCompositionDstu2.java
index 7169cacae64..883f8b292a1 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderCompositionDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderCompositionDstu2.java
@@ -1,24 +1,12 @@
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoComposition;
-import ca.uhn.fhir.jpa.util.JpaConstants;
-import ca.uhn.fhir.model.api.annotation.Description;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
-import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Composition;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
-import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
-import ca.uhn.fhir.rest.annotation.OperationParam;
-import ca.uhn.fhir.rest.annotation.Sort;
-import ca.uhn.fhir.rest.api.Constants;
-import ca.uhn.fhir.rest.api.SortSpec;
-import ca.uhn.fhir.rest.api.server.IBundleProvider;
-import ca.uhn.fhir.rest.api.server.RequestDetails;
-import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IBaseBundle;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
/*
* #%L
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderEncounterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderEncounterDstu2.java
index 2a3ab4e846e..82ed14f38fe 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderEncounterDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderEncounterDstu2.java
@@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.provider;
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoEncounter;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu2.resource.Encounter;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatientDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatientDstu2.java
index d5b69eb16f4..c4527298ce6 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatientDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatientDstu2.java
@@ -25,7 +25,7 @@ import java.util.List;
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoPatient;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.StringDt;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java
index 04768013c29..cfd022d161c 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java
@@ -28,7 +28,7 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem.LookupCodeResult;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BinaryAccessProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BinaryAccessProvider.java
new file mode 100644
index 00000000000..5be1610df99
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BinaryAccessProvider.java
@@ -0,0 +1,257 @@
+package ca.uhn.fhir.jpa.provider;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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.context.BaseRuntimeElementDefinition;
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
+import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
+import ca.uhn.fhir.jpa.dao.DaoRegistry;
+import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
+import ca.uhn.fhir.rest.annotation.IdParam;
+import ca.uhn.fhir.rest.annotation.Operation;
+import ca.uhn.fhir.rest.annotation.OperationParam;
+import ca.uhn.fhir.rest.api.Constants;
+import ca.uhn.fhir.rest.api.EncodingEnum;
+import ca.uhn.fhir.rest.server.RestfulServer;
+import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
+import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
+import ca.uhn.fhir.util.AttachmentUtil;
+import ca.uhn.fhir.util.DateUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.hl7.fhir.instance.model.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Optional;
+
+import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
+import static org.apache.commons.lang3.StringUtils.isBlank;
+
+/**
+ * This plain provider class can be registered with a JPA RestfulServer
+ * to provide the $binary-access-read
and $binary-access-write
+ * operations that can be used to access attachment data as a raw binary.
+ */
+public class BinaryAccessProvider {
+
+ @Autowired
+ private FhirContext myCtx;
+ @Autowired
+ private DaoRegistry myDaoRegistry;
+ @Autowired(required = false)
+ private IBinaryStorageSvc myBinaryStorageSvc;
+
+ /**
+ * $binary-access-read
+ */
+ @Operation(name = JpaConstants.OPERATION_BINARY_ACCESS_READ, global = true, manualResponse = true, idempotent = true)
+ public void binaryAccessRead(
+ @IdParam IIdType theResourceId,
+ @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType thePath,
+ ServletRequestDetails theRequestDetails,
+ HttpServletRequest theServletRequest,
+ HttpServletResponse theServletResponse) throws IOException {
+
+ validateResourceTypeAndPath(theResourceId, thePath);
+ IFhirResourceDao dao = getDaoForRequest(theResourceId);
+ IBaseResource resource = dao.read(theResourceId, theRequestDetails, false);
+
+ ICompositeType attachment = findAttachmentForRequest(resource, thePath, theRequestDetails);
+
+ IBaseHasExtensions attachmentHasExt = (IBaseHasExtensions) attachment;
+ Optional extends IBaseExtension, ?>> attachmentId = attachmentHasExt
+ .getExtension()
+ .stream()
+ .filter(t -> JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID.equals(t.getUrl()))
+ .findFirst();
+
+ if (attachmentId.isPresent()) {
+
+ @SuppressWarnings("unchecked")
+ IPrimitiveType value = (IPrimitiveType) attachmentId.get().getValue();
+ String blobId = value.getValueAsString();
+
+ IBinaryStorageSvc.StoredDetails blobDetails = myBinaryStorageSvc.fetchBlobDetails(theResourceId, blobId);
+ if (blobDetails == null) {
+ String msg = myCtx.getLocalizer().getMessage(BinaryAccessProvider.class, "unknownBlobId");
+ throw new InvalidRequestException(msg);
+ }
+
+ theServletResponse.setStatus(200);
+ theServletResponse.setContentType(blobDetails.getContentType());
+ if (blobDetails.getBytes() <= Integer.MAX_VALUE) {
+ theServletResponse.setContentLength((int) blobDetails.getBytes());
+ }
+
+ RestfulServer server = theRequestDetails.getServer();
+ server.addHeadersToResponse(theServletResponse);
+
+ theServletResponse.addHeader(Constants.HEADER_CACHE_CONTROL, Constants.CACHE_CONTROL_PRIVATE);
+ theServletResponse.addHeader(Constants.HEADER_ETAG, '"' + blobDetails.getHash() + '"');
+ theServletResponse.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(blobDetails.getPublished()));
+
+ myBinaryStorageSvc.writeBlob(theResourceId, blobId, theServletResponse.getOutputStream());
+ theServletResponse.getOutputStream().close();
+
+ myBinaryStorageSvc.writeBlob(theResourceId, blobId, theServletResponse.getOutputStream());
+
+ } else {
+
+ IPrimitiveType contentTypeDt = AttachmentUtil.getOrCreateContentType(theRequestDetails.getFhirContext(), attachment);
+ String contentType = contentTypeDt.getValueAsString();
+ contentType = StringUtils.defaultIfBlank(contentType, Constants.CT_OCTET_STREAM);
+
+ IPrimitiveType dataDt = AttachmentUtil.getOrCreateData(theRequestDetails.getFhirContext(), attachment);
+ byte[] data = dataDt.getValue();
+ if (data == null) {
+ String msg = myCtx.getLocalizer().getMessage(BinaryAccessProvider.class, "noAttachmentDataPresent", sanitizeUrlPart(theResourceId), sanitizeUrlPart(thePath));
+ throw new InvalidRequestException(msg);
+ }
+
+ theServletResponse.setStatus(200);
+ theServletResponse.setContentType(contentType);
+ theServletResponse.setContentLength(data.length);
+
+ RestfulServer server = theRequestDetails.getServer();
+ server.addHeadersToResponse(theServletResponse);
+
+ theServletResponse.getOutputStream().write(data);
+ theServletResponse.getOutputStream().close();
+
+ }
+ }
+
+ /**
+ * $binary-access-write
+ */
+ @SuppressWarnings("unchecked")
+ @Operation(name = JpaConstants.OPERATION_BINARY_ACCESS_WRITE, global = true, manualRequest = true, idempotent = false)
+ public IBaseResource binaryAccessWrite(
+ @IdParam IIdType theResourceId,
+ @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType thePath,
+ ServletRequestDetails theRequestDetails,
+ HttpServletRequest theServletRequest,
+ HttpServletResponse theServletResponse) throws IOException {
+
+ validateResourceTypeAndPath(theResourceId, thePath);
+ IFhirResourceDao dao = getDaoForRequest(theResourceId);
+ IBaseResource resource = dao.read(theResourceId, theRequestDetails, false);
+
+ ICompositeType attachment = findAttachmentForRequest(resource, thePath, theRequestDetails);
+
+ String requestContentType = theServletRequest.getContentType();
+ if (isBlank(requestContentType)) {
+ throw new InvalidRequestException("No content-attachment supplied");
+ }
+ if (EncodingEnum.forContentTypeStrict(requestContentType) != null) {
+ throw new InvalidRequestException("This operation is for binary content, got: " + requestContentType);
+ }
+
+ long size = theServletRequest.getContentLength();
+ String blobId = null;
+
+ if (size > 0) {
+ if (myBinaryStorageSvc != null) {
+ if (myBinaryStorageSvc.shouldStoreBlob(size, theResourceId, requestContentType)) {
+ IBinaryStorageSvc.StoredDetails storedDetails = myBinaryStorageSvc.storeBlob(theResourceId, requestContentType, theRequestDetails.getInputStream());
+ size = storedDetails.getBytes();
+ blobId = storedDetails.getBlobId();
+ Validate.notBlank(blobId, "BinaryStorageSvc returned a null blob ID"); // should not happen
+ }
+ }
+ }
+
+ if (blobId == null) {
+ byte[] bytes = IOUtils.toByteArray(theRequestDetails.getInputStream());
+ size = bytes.length;
+ AttachmentUtil.setData(theRequestDetails.getFhirContext(), attachment, bytes);
+ } else {
+
+ IBaseHasExtensions attachmentHasExt = (IBaseHasExtensions) attachment;
+ attachmentHasExt.getExtension().removeIf(t -> JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID.equals(t.getUrl()));
+ AttachmentUtil.setData(myCtx, attachment, null);
+
+ IBaseExtension, ?> ext = attachmentHasExt.addExtension();
+ ext.setUrl(JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID);
+ IPrimitiveType blobIdString = (IPrimitiveType) myCtx.getElementDefinition("string").newInstance();
+ blobIdString.setValueAsString(blobId);
+ ext.setValue(blobIdString);
+ }
+
+ AttachmentUtil.setContentType(theRequestDetails.getFhirContext(), attachment, requestContentType);
+
+ AttachmentUtil.setSize(theRequestDetails.getFhirContext(), attachment, null);
+ if (size <= Integer.MAX_VALUE) {
+ AttachmentUtil.setSize(theRequestDetails.getFhirContext(), attachment, (int) size);
+ }
+
+ DaoMethodOutcome outcome = dao.update(resource, theRequestDetails);
+ return outcome.getResource();
+ }
+
+ @Nonnull
+ private ICompositeType findAttachmentForRequest(IBaseResource theResource, @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType thePath, ServletRequestDetails theRequestDetails) {
+ FhirContext ctx = theRequestDetails.getFhirContext();
+ String path = thePath.getValueAsString();
+
+ Optional type = ctx.newFluentPath().evaluateFirst(theResource, path, ICompositeType.class);
+ if (!type.isPresent()) {
+ throw new InvalidRequestException("Unable to find Attachment at path: " + sanitizeUrlPart(path));
+ }
+
+ BaseRuntimeElementDefinition> def = ctx.getElementDefinition(type.get().getClass());
+ if (!def.getName().equals("Attachment")) {
+ throw new InvalidRequestException("Path does not return an Attachment: " + sanitizeUrlPart(path));
+ }
+ return type.get();
+ }
+
+ private void validateResourceTypeAndPath(@IdParam IIdType theResourceId, @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType thePath) {
+ if (isBlank(theResourceId.getResourceType())) {
+ throw new InvalidRequestException("No resource type specified");
+ }
+ if (isBlank(theResourceId.getIdPart())) {
+ throw new InvalidRequestException("No ID specified");
+ }
+ if (thePath == null || isBlank(thePath.getValue())) {
+ throw new InvalidRequestException("No path specified");
+ }
+ }
+
+ @Nonnull
+ private IFhirResourceDao getDaoForRequest(@IdParam IIdType theResourceId) {
+ String resourceType = theResourceId.getResourceType();
+ IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceType);
+ if (dao == null) {
+ throw new InvalidRequestException("Unknown/unsupported resource type: " + sanitizeUrlPart(resourceType));
+ }
+ return dao;
+ }
+
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java
index a4097ab081c..d20cf9a2c96 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java
@@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.provider;
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
@@ -38,7 +38,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.http.HttpServletRequest;
-import static ca.uhn.fhir.jpa.util.JpaConstants.*;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.*;
public class JpaResourceProviderDstu2 extends BaseJpaResourceProvider {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java
index 1f6f096dd4b..95c2fce80f5 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java
@@ -4,7 +4,7 @@ import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java
index b0545ea86a3..1d17bc8b98a 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.ISubscriptionTriggeringSvc;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java
index e83f8f64459..8e1546f1bb4 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem.LookupCodeResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCompositionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCompositionDstu3.java
index e8342babdb2..53327f9919e 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCompositionDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCompositionDstu3.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoComposition;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
@@ -13,13 +13,10 @@ import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
-import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
-import javax.print.attribute.standard.Severity;
-import java.util.ArrayList;
import java.util.List;
/*
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderConceptMapDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderConceptMapDstu3.java
index 44804c75597..ea66f294d43 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderConceptMapDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderConceptMapDstu3.java
@@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.term.TranslationRequest;
import ca.uhn.fhir.jpa.term.TranslationResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderEncounterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderEncounterDstu3.java
index 1527e15de2a..7cf5bf044e1 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderEncounterDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderEncounterDstu3.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.provider.dstu3;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import org.hl7.fhir.dstu3.model.*;
/*
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderPatientDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderPatientDstu3.java
index edde752fa7d..acc254b5f01 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderPatientDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderPatientDstu3.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoPatient;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java
index 92a95821268..8169da95cc5 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderStructureDefinitionDstu3.java
@@ -2,15 +2,13 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
-import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.UriParam;
-import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.ValidateUtil;
import org.hl7.fhir.dstu3.model.IdType;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java
index 0b6b217b87c..e55de8b4a51 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java
index 7cc6752119c..e031757ef31 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
@@ -38,9 +38,9 @@ import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.http.HttpServletRequest;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META_ADD;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META_DELETE;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_ADD;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_DELETE;
public class JpaResourceProviderDstu3 extends BaseJpaResourceProvider {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java
index 2493cf40dba..5643a9623c1 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java
@@ -4,7 +4,7 @@ import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java
index f36bef6879e..8c7f4e93732 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem.LookupCodeResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCompositionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCompositionR4.java
index d43e535923c..ef395292c0d 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCompositionR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCompositionR4.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoComposition;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderConceptMapR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderConceptMapR4.java
index cc0cc8a3f33..e13582a995e 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderConceptMapR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderConceptMapR4.java
@@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.term.TranslationRequest;
import ca.uhn.fhir.jpa.term.TranslationResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderEncounterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderEncounterR4.java
index ebe63013f67..a84de04ac48 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderEncounterR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderEncounterR4.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.provider.r4;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import org.hl7.fhir.r4.model.*;
/*
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java
index e8ab6f70bcc..f4158da734e 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java
@@ -1,17 +1,7 @@
package ca.uhn.fhir.jpa.provider.r4;
-import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader;
-import ca.uhn.fhir.jpa.util.JpaConstants;
-import ca.uhn.fhir.model.api.annotation.Description;
-import ca.uhn.fhir.rest.annotation.Operation;
-import ca.uhn.fhir.rest.annotation.OperationParam;
-import ca.uhn.fhir.rest.api.server.RequestDetails;
-import org.hl7.fhir.instance.model.api.IBaseBundle;
-import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.MessageHeader;
-import javax.servlet.http.HttpServletRequest;
-
/*
* #%L
* HAPI FHIR JPA Server
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderPatientR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderPatientR4.java
index ce01d02d0a5..dab5a522e2d 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderPatientR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderPatientR4.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoPatient;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java
index 150d63839ec..6db9e7a4001 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderStructureDefinitionR4.java
@@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoStructureDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java
index 031e30210d1..3bc61a2099d 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java
index 74360637d6c..6d6f60cdf6f 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java
@@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
@@ -35,9 +35,9 @@ import org.hl7.fhir.r4.model.*;
import javax.servlet.http.HttpServletRequest;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META_ADD;
-import static ca.uhn.fhir.jpa.util.JpaConstants.OPERATION_META_DELETE;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_ADD;
+import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_DELETE;
public class JpaResourceProviderR4 extends BaseJpaResourceProvider {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java
index d10436a0466..bc61881c6e2 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java
@@ -4,7 +4,7 @@ import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/ValueSetExpansionComponentWithCodeAccumulator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/ValueSetExpansionComponentWithCodeAccumulator.java
index 1fb98a9ff07..eb94f91d403 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/ValueSetExpansionComponentWithCodeAccumulator.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/ValueSetExpansionComponentWithCodeAccumulator.java
@@ -21,11 +21,14 @@ package ca.uhn.fhir.jpa.term;
*/
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
+import ca.uhn.fhir.model.api.annotation.Block;
import org.hl7.fhir.r4.model.ValueSet;
import java.util.Collection;
+@Block()
public class ValueSetExpansionComponentWithCodeAccumulator extends ValueSet.ValueSetExpansionComponent implements IValueSetCodeAccumulator {
+
@Override
public void includeCode(String theSystem, String theCode, String theDisplay) {
ValueSet.ValueSetExpansionContainsComponent contains = this.addContains();
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateDeserializer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateDeserializer.java
new file mode 100644
index 00000000000..5018bedb0bc
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateDeserializer.java
@@ -0,0 +1,44 @@
+package ca.uhn.fhir.jpa.util;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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 com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import org.hl7.fhir.dstu3.model.DateTimeType;
+
+import java.io.IOException;
+import java.util.Date;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public class JsonDateDeserializer extends JsonDeserializer {
+
+ @Override
+ public Date deserialize(JsonParser theParser, DeserializationContext theDeserializationContext) throws IOException {
+ String string = theParser.getValueAsString();
+ if (isNotBlank(string)) {
+ return new DateTimeType(string).getValue();
+ }
+ return null;
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateSerializer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateSerializer.java
new file mode 100644
index 00000000000..125e83cd304
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JsonDateSerializer.java
@@ -0,0 +1,40 @@
+package ca.uhn.fhir.jpa.util;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2019 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 com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import org.hl7.fhir.dstu3.model.InstantType;
+
+import java.io.IOException;
+import java.util.Date;
+
+public class JsonDateSerializer extends JsonSerializer {
+
+ @Override
+ public void serialize(Date theValue, JsonGenerator theGen, SerializerProvider theSerializers) throws IOException {
+ if (theValue != null) {
+ theGen.writeString(new InstantType(theValue).getValueAsString());
+ }
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java
index 60864857104..4f5c27daea8 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java
@@ -20,29 +20,39 @@ package ca.uhn.fhir.jpa.util;
* #L%
*/
+import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
+import org.hibernate.validator.constraints.Length;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.InstantType;
import javax.persistence.*;
+import javax.validation.constraints.Size;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
+import java.util.stream.Collectors;
+import static com.google.common.base.Ascii.toUpperCase;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class TestUtil {
+ public static final int MAX_COL_LENGTH = 2000;
private static final int MAX_LENGTH = 30;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
+ private static Set ourReservedWords;
/**
* non instantiable
@@ -56,6 +66,16 @@ public class TestUtil {
*/
@SuppressWarnings("UnstableApiUsage")
public static void scanEntities(String packageName) throws IOException, ClassNotFoundException {
+
+ try (InputStream is = TestUtil.class.getResourceAsStream("/mysql-reserved-words.txt")) {
+ String contents = IOUtils.toString(is, Constants.CHARSET_UTF8);
+ String[] words = contents.split("\\n");
+ ourReservedWords = Arrays.stream(words)
+ .filter(t -> isNotBlank(t))
+ .map(t -> toUpperCase(t))
+ .collect(Collectors.toSet());
+ }
+
ImmutableSet classes = ClassPath.from(TestUtil.class.getClassLoader()).getTopLevelClasses(packageName);
Set names = new HashSet();
@@ -138,7 +158,10 @@ public class TestUtil {
JoinColumn joinColumn = theAnnotatedElement.getAnnotation(JoinColumn.class);
if (joinColumn != null) {
- assertNotADuplicateName(joinColumn.name(), null);
+ String columnName = joinColumn.name();
+ validateColumnName(columnName, theAnnotatedElement);
+
+ assertNotADuplicateName(columnName, null);
ForeignKey fk = joinColumn.foreignKey();
if (theIsSuperClass) {
Validate.isTrue(isBlank(fk.name()), "Foreign key on " + theAnnotatedElement.toString() + " has a name() and should not as it is a superclass");
@@ -152,8 +175,47 @@ public class TestUtil {
Column column = theAnnotatedElement.getAnnotation(Column.class);
if (column != null) {
- assertNotADuplicateName(column.name(), null);
+ String columnName = column.name();
+ validateColumnName(columnName, theAnnotatedElement);
+
+ assertNotADuplicateName(columnName, null);
Validate.isTrue(column.unique() == false, "Should not use unique attribute on column (use named @UniqueConstraint instead) on " + theAnnotatedElement.toString());
+
+ boolean hasLob = theAnnotatedElement.getAnnotation(Lob.class) != null;
+ Field field = (Field) theAnnotatedElement;
+
+ /*
+ * For string columns, we want to make sure that an explicit max
+ * length is always specified, and that this max is always sensible.
+ * Unfortunately there is no way to differentiate between "explicitly
+ * set to 255" and "just using the default of 255" so we have banned
+ * the exact length of 255.
+ */
+ if (field.getType().equals(String.class)) {
+ if (!hasLob) {
+ if (column.length() == 255) {
+ throw new IllegalStateException("Field does not have an explicit maximum length specified: " + field);
+ }
+ if (column.length() > MAX_COL_LENGTH) {
+ throw new IllegalStateException("Field is too long: " + field);
+ }
+ }
+
+ Size size = theAnnotatedElement.getAnnotation(Size.class);
+ if (size != null) {
+ if (size.max() > MAX_COL_LENGTH) {
+ throw new IllegalStateException("Field is too long: " + field);
+ }
+ }
+
+ Length length = theAnnotatedElement.getAnnotation(Length.class);
+ if (length != null) {
+ if (length.max() > MAX_COL_LENGTH) {
+ throw new IllegalStateException("Field is too long: " + field);
+ }
+ }
+ }
+
}
GeneratedValue gen = theAnnotatedElement.getAnnotation(GeneratedValue.class);
@@ -169,6 +231,15 @@ public class TestUtil {
}
+ private static void validateColumnName(String theColumnName, AnnotatedElement theElement) {
+ if (!theColumnName.equals(theColumnName.toUpperCase())) {
+ throw new IllegalArgumentException("Column name must be all upper case: " + theColumnName + " found on " + theElement);
+ }
+ if (ourReservedWords.contains(theColumnName)) {
+ throw new IllegalArgumentException("Column name is a reserved word: " + theColumnName + " found on " + theElement);
+ }
+ }
+
private static void assertEquals(String theGenerator, String theName) {
Validate.isTrue(theGenerator.equals(theName));
}
@@ -209,4 +280,6 @@ public class TestUtil {
public static void sleepOneClick() {
sleepAtLeast(1);
}
+
+
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImplTest.java
new file mode 100644
index 00000000000..c4e770a8003
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImplTest.java
@@ -0,0 +1,21 @@
+package ca.uhn.fhir.jpa.binstore;
+
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.junit.Assert.assertThat;
+
+public class BaseBinaryStorageSvcImplTest {
+
+ private static final Logger ourLog = LoggerFactory.getLogger(BaseBinaryStorageSvcImplTest.class);
+
+ @Test
+ public void testNewRandomId() {
+ MemoryBinaryStorageSvcImpl svc = new MemoryBinaryStorageSvcImpl();
+ String id = svc.newRandomId();
+ ourLog.info(id);
+ assertThat(id, matchesPattern("^[a-zA-Z0-9]{100}$"));
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImplTest.java
new file mode 100644
index 00000000000..05af2cdfe7b
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/FilesystemBinaryStorageSvcImplTest.java
@@ -0,0 +1,59 @@
+package ca.uhn.fhir.jpa.binstore;
+
+import org.apache.commons.io.FileUtils;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.r4.model.IdType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+public class FilesystemBinaryStorageSvcImplTest {
+
+ private static final Logger ourLog = LoggerFactory.getLogger(FilesystemBinaryStorageSvcImplTest.class);
+ public static final byte[] SOME_BYTES = {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1};
+ private File myPath;
+ private FilesystemBinaryStorageSvcImpl mySvc;
+
+ @Before
+ public void before() {
+ myPath = new File("./target/fstmp");
+ mySvc = new FilesystemBinaryStorageSvcImpl(myPath.getAbsolutePath());
+ }
+
+ @After
+ public void after() throws IOException {
+ FileUtils.deleteDirectory(myPath);
+ }
+
+ @Test
+ public void testStoreAndRetrieve() throws IOException {
+ IIdType id = new IdType("Patient/123");
+ String contentType = "image/png";
+ IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(id, contentType, new ByteArrayInputStream(SOME_BYTES));
+
+ ourLog.info("Got id: {}", outcome);
+
+ IBinaryStorageSvc.StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
+ assertEquals(16L, details.getBytes());
+ assertEquals(outcome.getBlobId(), details.getBlobId());
+ assertEquals("image/png", details.getContentType());
+ assertEquals("dc7197cfab936698bef7818975c185a9b88b71a0a0a2493deea487706ddf20cb", details.getHash());
+ assertNotNull(details.getPublished());
+
+ ByteArrayOutputStream capture = new ByteArrayOutputStream();
+ mySvc.writeBlob(id, outcome.getBlobId(), capture);
+
+ assertArrayEquals(SOME_BYTES, capture.toByteArray());
+ }
+
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImplTest.java
new file mode 100644
index 00000000000..b8ca92aef9e
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImplTest.java
@@ -0,0 +1,31 @@
+package ca.uhn.fhir.jpa.binstore;
+
+import org.hl7.fhir.r4.model.IdType;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+
+public class NullBinaryStorageSvcImplTest {
+
+ private NullBinaryStorageSvcImpl mySvc = new NullBinaryStorageSvcImpl();
+
+ @Test
+ public void shouldStoreBlob() {
+ assertFalse(mySvc.shouldStoreBlob(1, new IdType("Patient/2"), "application/json"));
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void storeBlob() {
+ mySvc.storeBlob(null, null, null);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void fetchBlobDetails() {
+ mySvc.fetchBlobDetails(null, null);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void writeBlob() {
+ mySvc.writeBlob(null, null, null);
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java
index d7641c2b0f2..32adc439e02 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java
@@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.config;
+import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
+import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
@@ -156,6 +158,11 @@ public class TestR4Config extends BaseJavaConfigR4 {
return requestValidator;
}
+ @Bean
+ public IBinaryStorageSvc binaryStorage() {
+ return new MemoryBinaryStorageSvcImpl();
+ }
+
public static int getMaxThreads() {
return ourMaxThreads;
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java
index ae1bff90c99..597a012ab4c 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java
@@ -14,7 +14,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.term.VersionIndependentConcept;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.test.utilities.LoggingRule;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
index 40ede474561..6ddb2605da8 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
@@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
+import ca.uhn.fhir.jpa.provider.BinaryAccessProvider;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@@ -95,6 +96,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Qualifier("myAllergyIntoleranceDaoR4")
protected IFhirResourceDao myAllergyIntoleranceDao;
@Autowired
+ protected BinaryAccessProvider myBinaryAccessProvider;
+ @Autowired
protected ApplicationContext myAppCtx;
@Autowired
@Qualifier("myAppointmentDaoR4")
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java
index 1d3f9e7f381..78cc03b3e92 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Lists;
import org.hamcrest.Matchers;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java
index d06327a45f8..7b74d26d924 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java
@@ -60,7 +60,7 @@ import com.google.common.base.Charsets;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@@ -86,7 +86,6 @@ import ca.uhn.fhir.model.dstu2.resource.Location;
import ca.uhn.fhir.model.dstu2.resource.Medication;
import ca.uhn.fhir.model.dstu2.resource.MedicationAdministration;
import ca.uhn.fhir.model.dstu2.resource.MedicationOrder;
-import ca.uhn.fhir.model.dstu2.resource.MessageHeader;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Organization;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java
index f0adedab5a7..290c1cf7fa4 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java
@@ -1,12 +1,11 @@
package ca.uhn.fhir.jpa.provider.dstu3;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Composition;
-import org.hl7.fhir.dstu3.model.MessageHeader;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java
index bc64c37986f..ff9e7ca5ddd 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3StructureDefinitionTest.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.provider.dstu3;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.TestUtil;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderExpungeDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderExpungeDstu3Test.java
index e81000616b4..ccf5c032e6a 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderExpungeDstu3Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderExpungeDstu3Test.java
@@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
index 50ab28ac48a..fdf5e4d55a3 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
@@ -1,11 +1,9 @@
package ca.uhn.fhir.jpa.provider.r4;
-import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
-import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR4;
@@ -101,6 +99,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
if (ourServer == null) {
ourRestServer = new RestfulServer(myFhirCtx);
ourRestServer.registerProviders(myResourceProviders.createProviders());
+ ourRestServer.registerProvider(myBinaryAccessProvider);
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java
new file mode 100644
index 00000000000..00364e26f59
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java
@@ -0,0 +1,317 @@
+package ca.uhn.fhir.jpa.provider.r4;
+
+import ca.uhn.fhir.interceptor.api.HookParams;
+import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
+import ca.uhn.fhir.interceptor.api.Pointcut;
+import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
+import ca.uhn.fhir.rest.api.Constants;
+import com.google.common.base.Charsets;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.ContentType;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.r4.model.Attachment;
+import org.hl7.fhir.r4.model.DocumentReference;
+import org.hl7.fhir.r4.model.StringType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class BinaryAccessProviderR4Test extends BaseResourceProviderR4Test {
+
+ public static final byte[] SOME_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
+ public static final byte[] SOME_BYTES_2 = {5, 5, 5, 6};
+ private static final Logger ourLog = LoggerFactory.getLogger(BinaryAccessProviderR4Test.class);
+
+ @Autowired
+ private MemoryBinaryStorageSvcImpl myStorageSvc;
+
+ @Override
+ @Before
+ public void before() throws Exception {
+ super.before();
+ myStorageSvc.setMinSize(10);
+ }
+
+ @Override
+ @After
+ public void after() throws Exception {
+ super.after();
+ myStorageSvc.setMinSize(0);
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ IIdType id = createDocumentReference(true);
+
+ IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, interceptor);
+ doAnswer(t -> {
+ Pointcut pointcut = t.getArgument(0, Pointcut.class);
+ HookParams params = t.getArgument(1, HookParams.class);
+ ourLog.info("Interceptor invoked with pointcut {} and params {}", pointcut, params);
+ return null;
+ }).when(interceptor).invoke(any(), any());
+
+ // Read it back using the operation
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ +
+ "?path=DocumentReference.content.attachment";
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(200, resp.getStatusLine().getStatusCode());
+ assertEquals("image/png", resp.getEntity().getContentType().getValue());
+ assertEquals(SOME_BYTES.length, resp.getEntity().getContentLength());
+
+ byte[] actualBytes = IOUtils.toByteArray(resp.getEntity().getContent());
+ assertArrayEquals(SOME_BYTES, actualBytes);
+ }
+
+ verify(interceptor, times(1)).invoke(eq(Pointcut.STORAGE_PRESHOW_RESOURCES), any());
+
+ }
+
+
+ @Test
+ public void testReadSecondInstance() throws IOException {
+ IIdType id = createDocumentReference(true);
+
+ IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, interceptor);
+
+ // Read it back using the operation
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ +
+ "?path=DocumentReference.content[1].attachment";
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(200, resp.getStatusLine().getStatusCode());
+ assertEquals("image/gif", resp.getEntity().getContentType().getValue());
+ assertEquals(SOME_BYTES_2.length, resp.getEntity().getContentLength());
+
+ byte[] actualBytes = IOUtils.toByteArray(resp.getEntity().getContent());
+ assertArrayEquals(SOME_BYTES_2, actualBytes);
+ }
+
+ verify(interceptor, times(1)).invoke(eq(Pointcut.STORAGE_PRESHOW_RESOURCES), any());
+
+ }
+
+ @Test
+ public void testReadNoPath() throws IOException {
+ IIdType id = createDocumentReference(true);
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ;
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(400, resp.getStatusLine().getStatusCode());
+ String response = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
+ assertThat(response, containsString("No path specified"));
+
+ }
+
+ }
+
+
+ @Test
+ public void testReadNoData() throws IOException {
+ IIdType id = createDocumentReference(false);
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ +
+ "?path=DocumentReference.content.attachment";
+
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(400, resp.getStatusLine().getStatusCode());
+ String response = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
+ assertThat(response, matchesPattern(".*The resource with ID DocumentReference/[0-9]+ has no data at path.*"));
+
+ }
+
+ }
+
+ @Test
+ public void testReadUnknownBlobId() throws IOException {
+ IIdType id = createDocumentReference(false);
+
+ DocumentReference dr = ourClient.read().resource(DocumentReference.class).withId(id).execute();
+ dr.getContentFirstRep()
+ .getAttachment()
+ .addExtension(JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID, new StringType("AAAAA"));
+ ourClient.update().resource(dr).execute();
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ +
+ "?path=DocumentReference.content.attachment";
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(400, resp.getStatusLine().getStatusCode());
+ String response = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
+ assertThat(response, matchesPattern(".*Can not find the requested binary content. It may have been deleted.*"));
+
+ }
+
+ }
+
+ /**
+ * Stores a binary large enough that it should live in binary storage
+ */
+ @Test
+ public void testWriteLarge() throws IOException {
+ IIdType id = createDocumentReference(false);
+
+ IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, interceptor);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, interceptor);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, interceptor);
+
+ // Read it back using the operation
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_WRITE +
+ "?path=DocumentReference.content.attachment";
+ HttpPost post = new HttpPost(path);
+ post.setEntity(new ByteArrayEntity(SOME_BYTES, ContentType.IMAGE_JPEG));
+ post.addHeader("Accept", "application/fhir+json; _pretty=true");
+ String attachmentId;
+ try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
+
+ assertEquals(200, resp.getStatusLine().getStatusCode());
+ assertThat(resp.getEntity().getContentType().getValue(), containsString("application/fhir+json"));
+ String response = IOUtils.toString(resp.getEntity().getContent(), Constants.CHARSET_UTF8);
+ ourLog.info("Response: {}", response);
+
+ DocumentReference ref = myFhirCtx.newJsonParser().parseResource(DocumentReference.class, response);
+
+ Attachment attachment = ref.getContentFirstRep().getAttachment();
+ assertEquals(ContentType.IMAGE_JPEG.getMimeType(), attachment.getContentType());
+ assertEquals(15, attachment.getSize());
+ assertEquals(null, attachment.getData());
+ assertEquals("2", ref.getMeta().getVersionId());
+ attachmentId = attachment.getExtensionString(JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID);
+ assertThat(attachmentId, matchesPattern("[a-zA-Z0-9]{100}"));
+
+ }
+
+ verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESHOW_RESOURCES), any());
+ verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED), any());
+ verifyNoMoreInteractions(interceptor);
+
+ // Read it back using the operation
+
+ path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_READ +
+ "?path=DocumentReference.content.attachment";
+ HttpGet get = new HttpGet(path);
+ try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
+
+ assertEquals(200, resp.getStatusLine().getStatusCode());
+ assertEquals("image/jpeg", resp.getEntity().getContentType().getValue());
+ assertEquals(SOME_BYTES.length, resp.getEntity().getContentLength());
+
+ byte[] actualBytes = IOUtils.toByteArray(resp.getEntity().getContent());
+ assertArrayEquals(SOME_BYTES, actualBytes);
+ }
+
+ }
+
+ /**
+ * Stores a binary small enough that it shouldn't live in binary storage
+ */
+ @Test
+ public void testWriteSmall() throws IOException {
+ IIdType id = createDocumentReference(false);
+
+ IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, interceptor);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, interceptor);
+ myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, interceptor);
+
+ // Read it back using the operation
+
+ String path = ourServerBase +
+ "/DocumentReference/" + id.getIdPart() + "/" +
+ JpaConstants.OPERATION_BINARY_ACCESS_WRITE +
+ "?path=DocumentReference.content.attachment";
+ HttpPost post = new HttpPost(path);
+ post.setEntity(new ByteArrayEntity(SOME_BYTES_2, ContentType.IMAGE_JPEG));
+ post.addHeader("Accept", "application/fhir+json; _pretty=true");
+ String attachmentId;
+ try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
+
+ assertEquals(200, resp.getStatusLine().getStatusCode());
+ assertThat(resp.getEntity().getContentType().getValue(), containsString("application/fhir+json"));
+ String response = IOUtils.toString(resp.getEntity().getContent(), Constants.CHARSET_UTF8);
+ ourLog.info("Response: {}", response);
+
+ DocumentReference ref = myFhirCtx.newJsonParser().parseResource(DocumentReference.class, response);
+
+ Attachment attachment = ref.getContentFirstRep().getAttachment();
+ assertEquals(ContentType.IMAGE_JPEG.getMimeType(), attachment.getContentType());
+ assertEquals(4, attachment.getSize());
+ assertArrayEquals(SOME_BYTES_2, attachment.getData());
+ assertEquals("2", ref.getMeta().getVersionId());
+ attachmentId = attachment.getExtensionString(JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID);
+ assertEquals(null, attachmentId);
+
+ }
+
+ verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESHOW_RESOURCES), any());
+ verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED), any());
+ verifyNoMoreInteractions(interceptor);
+
+ }
+
+ private IIdType createDocumentReference(boolean theSetData) {
+ DocumentReference documentReference = new DocumentReference();
+ Attachment attachment = documentReference
+ .addContent()
+ .getAttachment()
+ .setContentType("image/png");
+ if (theSetData) {
+ attachment.setData(SOME_BYTES);
+ }
+ attachment = documentReference
+ .addContent()
+ .getAttachment()
+ .setContentType("image/gif");
+ if (theSetData) {
+ attachment.setData(SOME_BYTES_2);
+ }
+ return ourClient.create().resource(documentReference).execute().getId().toUnqualifiedVersionless();
+ }
+
+
+
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderExpungeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderExpungeR4Test.java
index 872ccf74dd4..c41bb4160f5 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderExpungeR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderExpungeR4Test.java
@@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java
index 0de047821cd..218e52dfa35 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java
@@ -1,9 +1,8 @@
package ca.uhn.fhir.jpa.provider.r4;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import ca.uhn.fhir.util.TestUtil;
-import org.hl7.fhir.r4.model.MessageHeader;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleType;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java
index 3edd7a1e79b..dbd30d04d4b 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemTest.java
@@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.TestUtil;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java
index ce11e9bc582..ad52e73f723 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4StructureDefinitionTest.java
@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.provider.r4;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.TestUtil;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java
index 3ce1f62e8dd..ee2be4fc725 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java
@@ -5,8 +5,7 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
-import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
@@ -15,7 +14,6 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.*;
-import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
@@ -72,7 +70,6 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
-import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.Matchers.*;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java
index c42849bc9a0..80c62546ce2 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java
@@ -1,11 +1,10 @@
package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.dao.DaoConfig;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
-import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.rest.api.MethodOutcome;
-import com.google.common.collect.Lists;
import com.icegreen.greenmail.store.FolderException;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
@@ -155,10 +154,10 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
- .setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
+ .setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("mailto:myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
- .setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
+ .setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
@@ -201,10 +200,10 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
Subscription subscriptionTemp = ourClient.read(Subscription.class, sub1.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
- .setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
+ .setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
- .setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
+ .setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java
index 38dca6bae5b..6cd7b161d3d 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java
@@ -3,10 +3,10 @@ package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.dao.DaoConfig;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.NotificationServlet;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
-import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.jpa.subscription.module.interceptor.SubscriptionDebugLogInterceptor;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy;
import ca.uhn.fhir.rest.annotation.Create;
@@ -158,7 +158,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
String databaseCriteria = "Observation?code=17861-6&context.type=IHD";
Subscription subscription = createSubscription(databaseCriteria, null, ourNotificationListenerServer);
List tag = subscription.getMeta().getTag();
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.get(0).getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.get(0).getSystem());
assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.get(0).getCode());
}
@@ -168,7 +168,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
Subscription subscription = createSubscription(inMemoryCriteria, null, ourNotificationListenerServer);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(subscription));
List tag = subscription.getMeta().getTag();
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.get(0).getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.get(0).getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.get(0).getCode());
}
@@ -468,7 +468,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
List tags = subscriptionOrig.getMeta().getTag();
assertEquals(1, tags.size());
Coding tag = tags.get(0);
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode());
assertEquals("In-memory", tag.getDisplay());
@@ -480,7 +480,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
tags = subscriptionActivated.getMeta().getTag();
assertEquals(1, tags.size());
tag = tags.get(0);
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode());
assertEquals("In-memory", tag.getDisplay());
}
@@ -495,7 +495,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
List tags = subscriptionOrig.getMeta().getTag();
assertEquals(1, tags.size());
Coding tag = tags.get(0);
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode());
assertEquals("Database", tag.getDisplay());
@@ -507,7 +507,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
tags = subscription.getMeta().getTag();
assertEquals(1, tags.size());
tag = tags.get(0);
- assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
+ assertEquals(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode());
assertEquals("Database", tag.getDisplay());
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java
index 6119168c8fb..cbe2bfc17ca 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java
@@ -1,13 +1,11 @@
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.jpa.config.StoppableSubscriptionDeliveringRestHookSubscriber;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test;
-import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
-import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
-import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.*;
@@ -302,7 +300,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
subscription1
.getChannel()
- .addExtension(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
+ .addExtension(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
ourLog.info("** About to update subscription");
int modCount = myCountingInterceptor.getSentCount();
@@ -378,7 +376,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription = newSubscription(criteria1, payload);
subscription
.getChannel()
- .addExtension(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
+ .addExtension(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
ourClient.create().resource(subscription).execute();
waitForActivatedSubscriptionCount(1);
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java
index 881f7e8630d..e571c931eb9 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java
@@ -6,7 +6,7 @@ import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.subscription.SubscriptionTriggeringSvcImpl;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTask.java
index 8e384081532..088929a0098 100644
--- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTask.java
+++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTask.java
@@ -33,6 +33,7 @@ public class RenameColumnTask extends BaseTableTask {
private static final Logger ourLog = LoggerFactory.getLogger(RenameColumnTask.class);
private String myOldName;
private String myNewName;
+ private boolean myAllowNeitherColumnToExist;
public void setOldName(String theOldName) {
Validate.notBlank(theOldName);
@@ -53,6 +54,9 @@ public class RenameColumnTask extends BaseTableTask {
throw new SQLException("Can not rename " + getTableName() + "." + myOldName + " to " + myNewName + " because both columns exist!");
}
if (!haveOldName && !haveNewName) {
+ if (isAllowNeitherColumnToExist()) {
+ return;
+ }
throw new SQLException("Can not rename " + getTableName() + "." + myOldName + " to " + myNewName + " because neither column exists!");
}
if (haveNewName) {
@@ -89,4 +93,12 @@ public class RenameColumnTask extends BaseTableTask {
executeSql(getTableName(), sql);
}
+
+ public void setAllowNeitherColumnToExist(boolean theAllowNeitherColumnToExist) {
+ myAllowNeitherColumnToExist = theAllowNeitherColumnToExist;
+ }
+
+ public boolean isAllowNeitherColumnToExist() {
+ return myAllowNeitherColumnToExist;
+ }
}
diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
index dd366cb8463..a086f6caa5b 100644
--- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
+++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
@@ -80,6 +80,16 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks {
.renameColumn("mySystemVersion", "SYSTEM_VERSION")
.renameColumn("myValueSet", "VALUESET_URL");
+ version.onTable("TRM_VALUESET")
+ .renameColumn("NAME", "VSNAME", true);
+
+ version.onTable("TRM_VALUESET_CONCEPT")
+ .renameColumn("CODE", "CODEVAL", true)
+ .renameColumn("SYSTEM", "SYSTEM_URL", true);
+
+ version.onTable("TRM_CONCEPT")
+ .renameColumn("CODE", "CODEVAL");
+
// TermValueSet
version.startSectionWithMessage("Processing table: TRM_VALUESET");
version.addIdGenerator("SEQ_VALUESET_PID");
diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java
index 421a0590e88..f4d3a07cb13 100644
--- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java
+++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java
@@ -168,10 +168,15 @@ public class BaseMigrationTasks {
}
public BuilderWithTableName renameColumn(String theOldName, String theNewName) {
+ return renameColumn(theOldName, theNewName, false);
+ }
+
+ public BuilderWithTableName renameColumn(String theOldName, String theNewName, boolean theAllowNeitherColumnToExist) {
RenameColumnTask task = new RenameColumnTask();
task.setTableName(myTableName);
task.setOldName(theOldName);
task.setNewName(theNewName);
+ task.setAllowNeitherColumnToExist(theAllowNeitherColumnToExist);
addTask(task);
return this;
}
diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTaskTest.java
index 9a3d1e79be8..fb2bc53cad4 100644
--- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTaskTest.java
+++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/RenameColumnTaskTest.java
@@ -45,7 +45,7 @@ public class RenameColumnTaskTest extends BaseTest {
}
@Test
- public void testNeitherColumnExists() throws SQLException {
+ public void testNeitherColumnExists() {
executeSql("create table SOMETABLE (PID bigint not null)");
RenameColumnTask task = new RenameColumnTask();
@@ -65,7 +65,21 @@ public class RenameColumnTaskTest extends BaseTest {
}
@Test
- public void testBothColumnsExist() throws SQLException {
+ public void testNeitherColumnExistsButAllowed() {
+ executeSql("create table SOMETABLE (PID bigint not null)");
+
+ RenameColumnTask task = new RenameColumnTask();
+ task.setTableName("SOMETABLE");
+ task.setOldName("myTextCol");
+ task.setNewName("TEXTCOL");
+ task.setAllowNeitherColumnToExist(true);
+ getMigrator().addTask(task);
+
+ getMigrator().migrate();
+ }
+
+ @Test
+ public void testBothColumnsExist() {
executeSql("create table SOMETABLE (PID bigint not null, PID2 bigint)");
RenameColumnTask task = new RenameColumnTask();
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java
index aa1dfddebe2..e3135481929 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java
@@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.model.entity;
*/
import ca.uhn.fhir.model.api.IQueryParameterType;
+import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
@@ -64,7 +65,7 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
private Long myResourcePid;
@Field()
- @Column(name = "RES_TYPE", nullable = false)
+ @Column(name = "RES_TYPE", nullable = false, length = Constants.MAX_RESOURCE_NAME_LENGTH)
private String myResourceType;
@Field()
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java
index 010a16ec93a..cd1bcaa6198 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java
@@ -47,12 +47,13 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
/*
* Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here
*/
- public static final int MAX_LENGTH = 255;
+ public static final int MAX_LENGTH = 254;
private static final long serialVersionUID = 1L;
@Column(name = "SP_URI", nullable = true, length = MAX_LENGTH)
@Field()
public String myUri;
+
@Id
@SequenceGenerator(name = "SEQ_SPIDX_URI", sequenceName = "SEQ_SPIDX_URI")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_URI")
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java
similarity index 62%
rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java
rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java
index 23ddd3c4714..8920d443f40 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java
@@ -1,8 +1,8 @@
-package ca.uhn.fhir.jpa.util;
+package ca.uhn.fhir.jpa.model.util;
/*-
* #%L
- * HAPI FHIR JPA Server
+ * HAPI FHIR Model
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@@ -147,4 +147,69 @@ public class JpaConstants {
* Operation name for the "$snapshot" operation
*/
public static final String OPERATION_SNAPSHOT = "$snapshot";
+
+ /**
+ * Operation name for the "$binary-access" operation
+ */
+ public static final String OPERATION_BINARY_ACCESS_READ = "$binary-access-read";
+
+ /**
+ * Operation name for the "$binary-access" operation
+ */
+ public static final String OPERATION_BINARY_ACCESS_WRITE = "$binary-access-write";
+
+ /**
+ *
+ * This extension should be of type string
and should be
+ * placed on the Subscription.channel
element
+ *
+ */
+ public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
+
+ /**
+ * This extension URL indicates whether a REST HOOK delivery should
+ * include the version ID when delivering.
+ *
+ * This extension should be of type boolean
and should be
+ * placed on the Subscription.channel
element.
+ *
+ */
+ public static final String EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids";
+
+ /**
+ * This extension URL indicates whether a REST HOOK delivery should
+ * reload the resource and deliver the latest version always. This
+ * could be useful for example if a resource which triggers a
+ * subscription gets updated many times in short succession and there
+ * is no value in delivering the older versions.
+ *
+ * Note that if the resource is now deleted, this may cause
+ * the delivery to be cancelled altogether.
+ *
+ *
+ *
+ * This extension should be of type boolean
and should be
+ * placed on the Subscription.channel
element.
+ *
+ */
+ public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version";
+
+ /**
+ * Indicate which strategy will be used to match this subscription
+ */
+ public static final String EXT_SUBSCRIPTION_MATCHING_STRATEGY = "http://hapifhir.io/fhir/StructureDefinition/subscription-matching-strategy";
+
+ /**
+ *
+ * This extension should be of type string
and should be
+ * placed on the Subscription.channel
element
+ *
+ */
+ public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
+
+ /**
+ * Extension ID for external binary references
+ */
+ public static final String EXT_ATTACHMENT_EXTERNAL_BINARY_ID = "http://hapifhir.io/fhir/StructureDefinition/attachment-external-binary-id";
+
}
diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java
index acfc737fcda..14a4bcd480a 100644
--- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java
+++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java
@@ -61,7 +61,11 @@ public class Retrier {
@Override
public void onError(RetryContext context, RetryCallback callback, Throwable throwable) {
super.onError(context, callback, throwable);
- ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.getMessage());
+ if (throwable instanceof NullPointerException) {
+ ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.getMessage(), throwable);
+ } else {
+ ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.getMessage());
+ }
}
};
myRetryTemplate.registerListener(listener);
diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java
index 44524b99509..1766c99c55a 100644
--- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java
+++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java
@@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy;
@@ -111,8 +112,8 @@ public class SubscriptionCanonicalizer {
String subjectTemplate;
try {
- from = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
- subjectTemplate = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
+ from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
+ subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
@@ -125,8 +126,8 @@ public class SubscriptionCanonicalizer {
String stripVersionIds;
String deliverLatestVersion;
try {
- stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
- deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
+ stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
+ deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
@@ -198,8 +199,8 @@ public class SubscriptionCanonicalizer {
String from;
String subjectTemplate;
try {
- from = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
- subjectTemplate = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
+ from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
+ subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
@@ -211,8 +212,8 @@ public class SubscriptionCanonicalizer {
String stripVersionIds;
String deliverLatestVersion;
try {
- stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
- deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
+ stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
+ deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
@@ -259,7 +260,7 @@ public class SubscriptionCanonicalizer {
} else {
throw new IllegalStateException("Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy);
}
- meta.addTag().setSystem(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display);
+ meta.addTag().setSystem(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display);
}
public String getSubscriptionStatus(IBaseResource theSubscription) {
diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java
index 1e34f14097e..6bfb148777b 100644
--- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java
+++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java
@@ -24,57 +24,6 @@ import org.hl7.fhir.dstu2.model.Subscription;
public class SubscriptionConstants {
- /**
- *
- * This extension should be of type string
and should be
- * placed on the Subscription.channel
element
- *
- */
- public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
-
- /**
- *
- * This extension should be of type string
and should be
- * placed on the Subscription.channel
element
- *
- */
- public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
-
-
- /**
- * This extension URL indicates whether a REST HOOK delivery should
- * include the version ID when delivering.
- *
- * This extension should be of type boolean
and should be
- * placed on the Subscription.channel
element.
- *
- */
- public static final String EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids";
-
- /**
- * This extension URL indicates whether a REST HOOK delivery should
- * reload the resource and deliver the latest version always. This
- * could be useful for example if a resource which triggers a
- * subscription gets updated many times in short succession and there
- * is no value in delivering the older versions.
- *
- * Note that if the resource is now deleted, this may cause
- * the delivery to be cancelled altogether.
- *
- *
- *
- * This extension should be of type boolean
and should be
- * placed on the Subscription.channel
element.
- *
- */
- public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version";
-
- /**
- * Indicate which strategy will be used to match this subscription
- */
-
- public static final String EXT_SUBSCRIPTION_MATCHING_STRATEGY = "http://hapifhir.io/fhir/StructureDefinition/subscription-matching-strategy";
-
/**
* The number of threads used in subscription channel processing
diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java
index 200a0f9cff1..592ed5becb0 100644
--- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java
+++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java
@@ -2,7 +2,7 @@ package ca.uhn.fhirtest.interceptor;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider;
import ca.uhn.fhir.jpa.provider.BaseTerminologyUploaderProvider;
-import ca.uhn.fhir.jpa.util.JpaConstants;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java
index ef2b7e43e2f..c43860e4296 100644
--- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java
+++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java
@@ -1048,7 +1048,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer extends InterceptorAdapter {
+@Interceptor
+abstract class BaseValidatingInterceptor {
/**
* Default value:
diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.java
index 48d0fc7a879..a602c642da9 100644
--- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.java
+++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.java
@@ -27,6 +27,8 @@ import java.nio.charset.Charset;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import ca.uhn.fhir.interceptor.api.Hook;
+import ca.uhn.fhir.interceptor.api.Pointcut;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
@@ -67,7 +69,7 @@ public class RequestValidatingInterceptor extends BaseValidatingInterceptor();
+ myExcludeOperationTypes = new HashSet<>();
}
myExcludeOperationTypes.add(theOperationType);
}
@@ -64,7 +66,7 @@ public class ResponseValidatingInterceptor extends BaseValidatingInterceptor theServer, RequestDetails theRequest) {
Object[] params = createMethodParams(theRequest);
-
-
Object resultObj = invokeServer(theServer, theRequest, params);
+ if (resultObj == null) {
+ return null;
+ }
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
@@ -375,6 +378,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
public Object invokeServer(IRestfulServer> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
IBaseResource response = doInvokeServer(theServer, theRequest);
+ if (response == null) {
+ return null;
+ }
Set summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
@@ -407,6 +413,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
BUNDLE_RESOURCE,
LIST_OF_RESOURCES,
METHOD_OUTCOME,
+ VOID,
RESOURCE
}
diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java
index a74f7ee68c7..c1c4bf0a226 100644
--- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java
+++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java
@@ -61,16 +61,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
private final String myName;
private final RestOperationTypeEnum myOtherOperationType;
private final ReturnTypeEnum myReturnType;
+ private boolean myGlobal;
private BundleTypeEnum myBundleType;
private boolean myCanOperateAtInstanceLevel;
private boolean myCanOperateAtServerLevel;
private boolean myCanOperateAtTypeLevel;
private String myDescription;
private List myReturnParams;
+ private boolean myManualRequestMode;
+ private boolean myManualResponseMode;
protected OperationMethodBinding(Class> theReturnResourceType, Class extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
- boolean theIdempotent, String theOperationName, Class extends IBaseResource> theOperationType,
- OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
+ boolean theIdempotent, String theOperationName, Class extends IBaseResource> theOperationType,
+ OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
super(theReturnResourceType, theMethod, theContext, theProvider);
myBundleType = theBundleType;
@@ -89,7 +92,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (isBlank(theOperationName)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
- + " but this annotation has no name defined");
+ + " but this annotation has no name defined");
}
if (theOperationName.startsWith("$") == false) {
theOperationName = "$" + theOperationName;
@@ -99,10 +102,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (theReturnTypeFromRp != null) {
setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
} else if (Modifier.isAbstract(theOperationType.getModifiers()) == false) {
- setResourceName(theContext.getResourceDefinition(theOperationType).getName());
- } else {
- setResourceName(null);
- }
+ setResourceName(theContext.getResourceDefinition(theOperationType).getName());
+ } else {
+ setResourceName(null);
+ }
if (theMethod.getReturnType().equals(IBundleProvider.class)) {
myReturnType = ReturnTypeEnum.BUNDLE;
@@ -110,24 +113,24 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
myReturnType = ReturnTypeEnum.RESOURCE;
}
- myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
+ myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
if (getResourceName() == null) {
- myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
- myCanOperateAtServerLevel = true;
- if (myIdParamIndex != null) {
- myCanOperateAtInstanceLevel = true;
- }
+ myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
+ myCanOperateAtServerLevel = true;
+ if (myIdParamIndex != null) {
+ myCanOperateAtInstanceLevel = true;
+ }
} else if (myIdParamIndex == null) {
- myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
- myCanOperateAtTypeLevel = true;
+ myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
+ myCanOperateAtTypeLevel = true;
} else {
- myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
- myCanOperateAtInstanceLevel = true;
- for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
- if (next instanceof IdParam) {
- myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
- }
- }
+ myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
+ myCanOperateAtInstanceLevel = true;
+ for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
+ if (next instanceof IdParam) {
+ myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
+ }
+ }
}
myReturnParams = new ArrayList<>();
@@ -151,10 +154,23 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
+ /**
+ * Constructor - This is the constructor that is called when binding a
+ * standard @Operation method.
+ */
public OperationMethodBinding(Class> theReturnResourceType, Class extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
- Operation theAnnotation) {
+ Operation theAnnotation) {
this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(),
- theAnnotation.bundleType());
+ theAnnotation.bundleType());
+
+ myManualRequestMode = theAnnotation.manualRequest();
+ myManualResponseMode = theAnnotation.manualResponse();
+ myGlobal = theAnnotation.global();
+ }
+
+ @Override
+ public boolean isGlobalMethod() {
+ return myGlobal;
}
public String getDescription() {
@@ -206,7 +222,9 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) {
- return false;
+ if (!isGlobalMethod()) {
+ return false;
+ }
}
}
@@ -222,12 +240,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
boolean requestHasId = theRequest.getId() != null;
if (requestHasId) {
- return myCanOperateAtInstanceLevel;
- }
- if (isNotBlank(theRequest.getResourceName())) {
- return myCanOperateAtTypeLevel;
- }
- return myCanOperateAtServerLevel;
+ return myCanOperateAtInstanceLevel;
+ }
+ if (isNotBlank(theRequest.getResourceName())) {
+ return myCanOperateAtTypeLevel;
+ }
+ return myCanOperateAtServerLevel;
}
@Override
@@ -240,6 +258,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
+ if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) {
+ retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
+ } else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) {
+ retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
+ }
+
return retVal;
}
@@ -256,7 +280,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
@Override
public Object invokeServer(IRestfulServer> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
- if (theRequest.getRequestType() == RequestTypeEnum.POST) {
+ if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) {
IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
}
@@ -286,6 +310,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
+ if (myManualResponseMode) {
+ return null;
+ }
+
IBundleProvider retVal = toResourceList(response);
return retVal;
}
@@ -316,6 +344,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
+ public boolean isManualRequestMode() {
+ return myManualRequestMode;
+ }
+
public static class ReturnType {
private int myMax;
private int myMin;
diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java
index 5e73e27b693..8aa920902c9 100644
--- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java
+++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java
@@ -210,7 +210,9 @@ public class OperationParameter implements IParameter {
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding> theMethodBinding) throws InternalErrorException, InvalidRequestException {
List matchingParamValues = new ArrayList();
- if (theRequest.getRequestType() == RequestTypeEnum.GET) {
+ OperationMethodBinding method = (OperationMethodBinding) theMethodBinding;
+
+ if (theRequest.getRequestType() == RequestTypeEnum.GET || method.isManualRequestMode()) {
translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
} else {
translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);
diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java
index b4ad12812cb..c7617181893 100644
--- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java
+++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java
@@ -7,9 +7,14 @@ import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
+import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
+import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
+import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
+import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
+import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
@@ -17,6 +22,7 @@ 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.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
@@ -26,6 +32,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
import org.junit.AfterClass;
@@ -33,16 +41,18 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
-import ca.uhn.fhir.test.utilities.JettyUtil;
-
public class OperationServerR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerR4Test.class);
private static final String TEXT_HTML = "text/html";
@@ -57,6 +67,7 @@ public class OperationServerR4Test {
private static UnsignedIntType ourLastParamUnsignedInt1;
private static int ourPort;
private static Server ourServer;
+ private static IBaseResource ourNextResponse;
private IGenericClient myFhirClient;
@Before
@@ -69,11 +80,11 @@ public class OperationServerR4Test {
ourLastId = null;
ourLastMethod = "";
ourNextResponse = null;
+ ourLastRestOperation = null;
myFhirClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
}
-
@Test
public void testConformance() {
LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
@@ -150,7 +161,7 @@ public class OperationServerR4Test {
bundle.addEntry().setResource(patient);
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE_RETURNING_BUNDLE"
- + "?_pretty=true&_elements=identifier");
+ + "?_pretty=true&_elements=identifier");
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
assertEquals(200, status.getStatusLine().getStatusCode());
@@ -165,6 +176,24 @@ public class OperationServerR4Test {
}
+
+ @Test
+ public void testManualResponseWithPrimitiveParam() throws Exception {
+
+ // Try with a GET
+ HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$manualResponseWithPrimitiveParam?path=THIS_IS_A_PATH");
+ try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
+ assertEquals(200, status.getStatusLine().getStatusCode());
+ String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
+ }
+
+ assertEquals("$manualResponseWithPrimitiveParam", ourLastMethod);
+ assertEquals("Patient/123", ourLastId.toUnqualifiedVersionless().getValue());
+ assertEquals("THIS_IS_A_PATH", ourLastParam1.getValue());
+
+ }
+
+
@Test
public void testInstanceEverythingGet() throws Exception {
@@ -181,6 +210,23 @@ public class OperationServerR4Test {
}
+ @Test
+ public void testInstanceOnPlainProvider() throws Exception {
+
+ // Try with a GET
+ HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$OP_PLAIN_PROVIDER_ON_INSTANCE");
+ try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
+ assertEquals(200, status.getStatusLine().getStatusCode());
+ String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
+ assertThat(response, startsWith(" resources = new ArrayList();
for (int i = 0; i < 100; i++) {
@@ -780,7 +898,38 @@ public class OperationServerR4Test {
return new SimpleBundleProvider(resources);
}
- @Operation(name = "$OP_SERVER")
+
+ @Operation(name = "$OP_SERVER_BUNDLE_PROVIDER", idempotent = true)
+ public IBundleProvider opInstanceReturnsBundleProvider() {
+ ourLastMethod = "$OP_SERVER_BUNDLE_PROVIDER";
+
+ List resources = new ArrayList();
+ for (int i = 0; i < 100; i++) {
+ Patient p = new Patient();
+ p.setId("Patient/" + i);
+ p.addName().setFamily("Patient " + i);
+ resources.add(p);
+ }
+
+ return new SimpleBundleProvider(resources);
+ }
+
+ @Operation(name= "$manualResponseWithPrimitiveParam", idempotent = true, global = true, manualResponse = true)
+ public void binaryAccess(
+ @IdParam IIdType theResourceId,
+ @OperationParam(name="path", min = 1, max = 1) IPrimitiveType thePath,
+ ServletRequestDetails theRequestDetails,
+ HttpServletRequest theServletRequest,
+ HttpServletResponse theServletResponse) {
+
+ ourLastMethod = "$manualResponseWithPrimitiveParam";
+ ourLastId = (IdType) theResourceId;
+ ourLastParam1 = (StringType) thePath;
+
+ theServletResponse.setStatus(200);
+ }
+
+ @Operation(name = "$OP_SERVER")
public Parameters opServer(
@OperationParam(name = "PARAM1") StringType theParam1,
@OperationParam(name = "PARAM2") Patient theParam2
@@ -859,13 +1008,14 @@ public class OperationServerR4Test {
servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2));
servlet.setFhirContext(ourCtx);
- servlet.setResourceProviders(new PatientProvider());
- servlet.setPlainProviders(new PlainProvider());
+ PlainProvider plainProvider = new PlainProvider();
+ PatientProvider patientProvider = new PatientProvider();
+ servlet.registerProviders(patientProvider, plainProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
- ourPort = JettyUtil.getPortForStartedServer(ourServer);
+ ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
diff --git a/hapi-fhir-test-utilities/src/main/resources/mysql-reserved-words.txt b/hapi-fhir-test-utilities/src/main/resources/mysql-reserved-words.txt
new file mode 100644
index 00000000000..9a60b019a5c
--- /dev/null
+++ b/hapi-fhir-test-utilities/src/main/resources/mysql-reserved-words.txt
@@ -0,0 +1,818 @@
+ACCESSIBLE
+ACCOUNT
+ACTION
+ACTIVE
+ADD
+ADMIN
+AFTER
+AGAINST
+AGGREGATE
+ALGORITHM
+ALL
+ALTER
+ALWAYS
+ANALYSE
+ANALYZE
+AND
+ANY
+ARRAY
+AS
+ASC
+ASCII
+ASENSITIVE
+AT
+AUTOEXTEND_SIZE
+AUTO_INCREMENT
+AVG
+AVG_ROW_LENGTH
+B
+BACKUP
+BEFORE
+BEGIN
+BETWEEN
+BIGINT
+BINARY
+BINLOG
+BIT
+BLOB
+BLOCK
+BOOL
+BOOLEAN
+BOTH
+BTREE
+BUCKETS
+BY
+BYTE
+C
+CACHE
+CALL
+CASCADE
+CASCADED
+CASE
+CATALOG_NAME
+CHAIN
+CHANGE
+CHANGED
+CHANNEL
+CHAR
+CHARACTER
+CHARSET
+CHECK
+CHECKSUM
+CIPHER
+CLASS_ORIGIN
+CLIENT
+CLONE
+CLOSE
+COALESCE
+CODE
+COLLATE
+COLLATION
+COLUMN
+COLUMNS
+COLUMN_FORMAT
+COLUMN_NAME
+COMMENT
+COMMIT
+COMMITTED
+COMPACT
+COMPLETION
+COMPONENT
+COMPRESSED
+COMPRESSION
+CONCURRENT
+CONDITION
+CONNECTION
+CONSISTENT
+CONSTRAINT
+CONSTRAINT_CATALOG
+CONSTRAINT_NAME
+CONSTRAINT_SCHEMA
+CONTAINS
+CONTEXT
+CONTINUE
+CONVERT
+CPU
+CREATE
+CROSS
+CUBE
+CUME_DIST
+CURRENT
+CURRENT_DATE
+CURRENT_TIME
+CURRENT_TIMESTAMP
+CURRENT_USER
+CURSOR
+CURSOR_NAME
+D
+DATA
+DATABASE
+DATABASES
+DATAFILE
+DATE
+DATETIME
+DAY
+DAY_HOUR
+DAY_MICROSECOND
+DAY_MINUTE
+DAY_SECOND
+DEALLOCATE
+DEC
+DECIMAL
+DECLARE
+DEFAULT
+DEFAULT_AUTH
+DEFINER
+DEFINITION
+DELAYED
+DELAY_KEY_WRITE
+DELETE
+DENSE_RANK
+DESC
+DESCRIBE
+DESCRIPTION
+DES_KEY_FILE
+DETERMINISTIC
+DIAGNOSTICS
+DIRECTORY
+DISABLE
+DISCARD
+DISK
+DISTINCT
+DISTINCTROW
+DIV
+DO
+DOUBLE
+DROP
+DUAL
+DUMPFILE
+DUPLICATE
+DYNAMIC
+E
+EACH
+ELSE
+ELSEIF
+EMPTY
+ENABLE
+ENCLOSED
+ENCRYPTION
+END
+ENDS
+ENFORCED
+ENGINE
+ENGINES
+ENUM
+ERROR
+ERRORS
+ESCAPE
+ESCAPED
+EVENT
+EVENTS
+EVERY
+EXCEPT
+EXCHANGE
+EXCLUDE
+EXECUTE
+EXISTS
+EXIT
+EXPANSION
+EXPIRE
+EXPLAIN
+EXPORT
+EXTENDED
+EXTENT_SIZE
+F
+FALSE
+FAST
+FAULTS
+FETCH
+FIELDS
+FILE
+FILE_BLOCK_SIZE
+FILTER
+FIRST
+FIRST_VALUE
+FIXED
+FLOAT
+FLUSH
+FOLLOWING
+FOLLOWS
+FOR
+FORCE
+FOREIGN
+FORMAT
+FOUND
+FROM
+FULL
+FULLTEXT
+FUNCTION
+G
+GENERAL
+GENERATED
+GEOMCOLLECTION
+GEOMETRY
+GEOMETRYCOLLECTION
+GET
+GET_FORMAT
+GET_MASTER_PUBLIC_KEY
+GLOBAL
+GRANT
+GRANTS
+GROUP
+GROUPING
+GROUPS
+GROUP_REPLICATION
+H
+HANDLER
+HASH
+HAVING
+HELP
+HIGH_PRIORITY
+HISTOGRAM
+HISTORY
+HOST
+HOSTS
+HOUR
+HOUR_MICROSECOND
+HOUR_MINUTE
+HOUR_SECOND
+I
+IDENTIFIED
+IF
+IGNORE
+IGNORE_SERVER_IDS
+IMPORT
+IN
+INACTIVE
+INDEX
+INDEXES
+INFILE
+INITIAL_SIZE
+INNER
+INOUT
+INSENSITIVE
+INSERT
+INSERT_METHOD
+INSTALL
+INSTANCE
+INT
+INTEGER
+INTERVAL
+INTO
+INVISIBLE
+INVOKER
+IO
+IO_AFTER_GTIDS
+IO_BEFORE_GTIDS
+IO_THREAD
+IPC
+IS
+ISOLATION
+ISSUER
+ITERATE
+J
+JOIN
+JSON
+JSON_TABLE
+K
+KEY
+KEYS
+KEY_BLOCK_SIZE
+KILL
+L
+LAG
+LANGUAGE
+LAST
+LAST_VALUE
+LATERAL
+LEAD
+LEADING
+LEAVE
+LEAVES
+LEFT
+LESS
+LEVEL
+LIKE
+LIMIT
+LINEAR
+LINES
+LINESTRING
+LIST
+LOAD
+LOCAL
+LOCALTIME
+LOCALTIMESTAMP
+LOCK
+LOCKED
+LOCKS
+LOGFILE
+LOGS
+LONG
+LONGBLOB
+LONGTEXT
+LOOP
+LOW_PRIORITY
+M
+MASTER
+MASTER_AUTO_POSITION
+MASTER_BIND
+MASTER_COMPRESSION_ALGORITHMS
+MASTER_CONNECT_RETRY
+MASTER_DELAY
+MASTER_HEARTBEAT_PERIOD
+MASTER_HOST
+MASTER_LOG_FILE
+MASTER_LOG_POS
+MASTER_PASSWORD
+MASTER_PORT
+MASTER_PUBLIC_KEY_PATH
+MASTER_RETRY_COUNT
+MASTER_SERVER_ID
+MASTER_SSL
+MASTER_SSL_CA
+MASTER_SSL_CAPATH
+MASTER_SSL_CERT
+MASTER_SSL_CIPHER
+MASTER_SSL_CRL
+MASTER_SSL_CRLPATH
+MASTER_SSL_KEY
+MASTER_SSL_VERIFY_SERVER_CERT
+MASTER_TLS_VERSION
+MASTER_USER
+MASTER_ZSTD_COMPRESSION_LEVEL
+MATCH
+MAXVALUE
+MAX_CONNECTIONS_PER_HOUR
+MAX_QUERIES_PER_HOUR
+MAX_ROWS
+MAX_SIZE
+MAX_UPDATES_PER_HOUR
+MAX_USER_CONNECTIONS
+MEDIUM
+MEDIUMBLOB
+MEDIUMINT
+MEDIUMTEXT
+MEMBER
+MEMORY
+MERGE
+MESSAGE_TEXT
+MICROSECOND
+MIDDLEINT
+MIGRATE
+MINUTE
+MINUTE_MICROSECOND
+MINUTE_SECOND
+MIN_ROWS
+MOD
+MODE
+MODIFIES
+MODIFY
+MONTH
+MULTILINESTRING
+MULTIPOINT
+MULTIPOLYGON
+MUTEX
+MYSQL_ERRNO
+N
+NAME
+NAMES
+NATIONAL
+NATURAL
+NCHAR
+NDB
+NDBCLUSTER
+NESTED
+NETWORK_NAMESPACE
+NEVER
+NEW
+NEXT
+NO
+NODEGROUP
+NONE
+NOT
+NOWAIT
+NO_WAIT
+NO_WRITE_TO_BINLOG
+NTH_VALUE
+NTILE
+NULL
+NULLS
+NUMBER
+NUMERIC
+NVARCHAR
+O
+OF
+OFFSET
+OJ
+OLD
+ON
+ONE
+ONLY
+OPEN
+OPTIMIZE
+OPTIMIZER_COSTS
+OPTION
+OPTIONAL
+OPTIONALLY
+OPTIONS
+OR
+ORDER
+ORDINALITY
+ORGANIZATION
+OTHERS
+OUT
+OUTER
+OUTFILE
+OVER
+OWNER
+P
+PACK_KEYS
+PAGE
+PARSER
+PARTIAL
+PARTITION
+PARTITIONING
+PARTITIONS
+PASSWORD
+PATH
+PERCENT_RANK
+PERSIST
+PERSIST_ONLY
+PHASE
+PLUGIN
+PLUGINS
+PLUGIN_DIR
+POINT
+POLYGON
+PORT
+PRECEDES
+PRECEDING
+PRECISION
+PREPARE
+PRESERVE
+PREV
+PRIMARY
+PRIVILEGES
+PROCEDURE
+PROCESS
+PROCESSLIST
+PROFILE
+PROFILES
+PROXY
+PURGE
+Q
+QUARTER
+QUERY
+QUICK
+R
+RANDOM
+RANGE
+RANK
+READ
+READS
+READ_ONLY
+READ_WRITE
+REAL
+REBUILD
+RECOVER
+RECURSIVE
+REDOFILE
+REDO_BUFFER_SIZE
+REDUNDANT
+REFERENCE
+REFERENCES
+REGEXP
+RELAY
+RELAYLOG
+RELAY_LOG_FILE
+RELAY_LOG_POS
+RELAY_THREAD
+RELEASE
+RELOAD
+REMOTE
+REMOVE
+RENAME
+REORGANIZE
+REPAIR
+REPEAT
+REPEATABLE
+REPLACE
+REPLICATE_DO_DB
+REPLICATE_DO_TABLE
+REPLICATE_IGNORE_DB
+REPLICATE_IGNORE_TABLE
+REPLICATE_REWRITE_DB
+REPLICATE_WILD_DO_TABLE
+REPLICATE_WILD_IGNORE_TABLE
+REPLICATION
+REQUIRE
+RESET
+RESIGNAL
+RESOURCE
+RESPECT
+RESTART
+RESTORE
+RESTRICT
+RESUME
+RETAIN
+RETURN
+RETURNED_SQLSTATE
+RETURNS
+REUSE
+REVERSE
+REVOKE
+RIGHT
+RLIKE
+ROLE
+ROLLBACK
+ROLLUP
+ROTATE
+ROUTINE
+ROW
+ROWS
+ROW_COUNT
+ROW_FORMAT
+ROW_NUMBER
+RTREE
+S
+SAVEPOINT
+SCHEDULE
+SCHEMA
+SCHEMAS
+SCHEMA_NAME
+SECOND
+SECONDARY
+SECONDARY_ENGINE
+SECONDARY_LOAD
+SECONDARY_UNLOAD
+SECOND_MICROSECOND
+SECURITY
+SELECT
+SENSITIVE
+SEPARATOR
+SERIAL
+SERIALIZABLE
+SERVER
+SESSION
+SET
+SHARE
+SHOW
+SHUTDOWN
+SIGNAL
+SIGNED
+SIMPLE
+SKIP
+SLAVE
+SLOW
+SMALLINT
+SNAPSHOT
+SOCKET
+SOME
+SONAME
+SOUNDS
+SOURCE
+SPATIAL
+SPECIFIC
+SQL
+SQLEXCEPTION
+SQLSTATE
+SQLWARNING
+SQL_AFTER_GTIDS
+SQL_AFTER_MTS_GAPS
+SQL_BEFORE_GTIDS
+SQL_BIG_RESULT
+SQL_BUFFER_RESULT
+SQL_CACHE
+SQL_CALC_FOUND_ROWS
+SQL_NO_CACHE
+SQL_SMALL_RESULT
+SQL_THREAD
+SQL_TSI_DAY
+SQL_TSI_HOUR
+SQL_TSI_MINUTE
+SQL_TSI_MONTH
+SQL_TSI_QUARTER
+SQL_TSI_SECOND
+SQL_TSI_WEEK
+SQL_TSI_YEAR
+SRID
+SSL
+STACKED
+START
+STARTING
+STARTS
+STATS_AUTO_RECALC
+STATS_PERSISTENT
+STATS_SAMPLE_PAGES
+STATUS
+STOP
+STORAGE
+STORED
+STRAIGHT_JOIN
+STRING
+SUBCLASS_ORIGIN
+SUBJECT
+SUBPARTITION
+SUBPARTITIONS
+SUPER
+SUSPEND
+SWAPS
+SWITCHES
+SYSTEM
+T
+TABLE
+TABLES
+TABLESPACE
+TABLE_CHECKSUM
+TABLE_NAME
+TEMPORARY
+TEMPTABLE
+TERMINATED
+TEXT
+THAN
+THEN
+THREAD_PRIORITY
+TIES
+TIME
+TIMESTAMP
+TIMESTAMPADD
+TIMESTAMPDIFF
+TINYBLOB
+TINYINT
+TINYTEXT
+TO
+TRAILING
+TRANSACTION
+TRIGGER
+TRIGGERS
+TRUE
+TRUNCATE
+TYPE
+TYPES
+U
+UNBOUNDED
+UNCOMMITTED
+UNDEFINED
+UNDO
+UNDOFILE
+UNDO_BUFFER_SIZE
+UNICODE
+UNINSTALL
+UNION
+UNIQUE
+UNKNOWN
+UNLOCK
+UNSIGNED
+UNTIL
+UPDATE
+UPGRADE
+USAGE
+USE
+USER
+USER_RESOURCES
+USE_FRM
+USING
+UTC_DATE
+UTC_TIME
+UTC_TIMESTAMP
+V
+VALIDATION
+VALUE
+VALUES
+VARBINARY
+VARCHAR
+VARCHARACTER
+VARIABLES
+VARYING
+VCPU
+VIEW
+VIRTUAL
+VISIBLE
+W
+WAIT
+WARNINGS
+WEEK
+WEIGHT_STRING
+WHEN
+WHERE
+WHILE
+WINDOW
+WITH
+WITHOUT
+WORK
+WRAPPER
+WRITE
+X
+XA
+XID
+XML
+XOR
+Y
+YEAR
+YEAR_MONTH
+Z
+ZEROFILL
+A
+ACTIVE
+ADMIN
+ARRAY
+B
+BUCKETS
+C
+CLONE
+COMPONENT
+CUME_DIST
+D
+DEFINITION
+DENSE_RANK
+DESCRIPTION
+E
+EMPTY
+ENFORCED
+EXCEPT
+EXCLUDE
+F
+FIRST_VALUE
+FOLLOWING
+G
+GEOMCOLLECTION
+GET_MASTER_PUBLIC_KEY
+GROUPING
+GROUPS
+H
+HISTOGRAM
+HISTORY
+I
+INACTIVE
+INVISIBLE
+J
+JSON_TABLE
+L
+LAG
+LAST_VALUE
+LATERAL
+LEAD
+LOCKED
+M
+MASTER_COMPRESSION_ALGORITHMS
+MASTER_PUBLIC_KEY_PATH
+MASTER_ZSTD_COMPRESSION_LEVEL
+MEMBER
+N
+NESTED
+NETWORK_NAMESPACE
+NOWAIT
+NTH_VALUE
+NTILE
+NULLS
+O
+OF
+OJ
+OLD
+OPTIONAL
+ORDINALITY
+ORGANIZATION
+OTHERS
+OVER
+P
+PATH
+PERCENT_RANK
+PERSIST
+PERSIST_ONLY
+PRECEDING
+PROCESS
+R
+RANDOM
+RANK
+RECURSIVE
+REFERENCE
+RESOURCE
+RESPECT
+RESTART
+RETAIN
+REUSE
+ROLE
+ROW_NUMBER
+S
+SECONDARY
+SECONDARY_ENGINE
+SECONDARY_LOAD
+SECONDARY_UNLOAD
+SKIP
+SRID
+SYSTEM
+T
+THREAD_PRIORITY
+TIES
+U
+UNBOUNDED
+V
+VCPU
+VISIBLE
+W
+WINDOW
+ANALYSE
+DES_KEY_FILE
+PARSE_GCOL_EXPR
+REDOFILE
+SQL_CACHE
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 834c56e100c..4b8dd99f5ab 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -296,6 +296,32 @@
Expanded ValueSets now populate ValueSet.expansion.contains.designation.language]]>.
+
+ @Operation methods can now declare that they will manually process the request
+ body and/or manually generate a response instead of letting the HAPI FHIR
+ framework take care of these things. This is useful for situations where
+ direct access to the low-level servlet streaming API is needed.
+
+
+ @Operation methods can now declare that they are global, meaning that they will
+ apply to all resource types (or instances of all resource types) if they
+ are found on a plain provider.
+
+
+ A new resource provider for JPA servers called
+ BinaryAccessProvider]]>
+ has been added. This provider serves two custom operations called
+ $binary-access-read]]> and
+ $binary-access-write]]> that can be used to
+ request binary data in Attachments as raw binary content instead of
+ as base 64 encoded content.
+
+
+ A few columns named 'CODE' in the JPA terminology services tables have been
+ renamed to 'CODEVAL' to avoid any possibility of conflicting with reserved
+ words in MySQL. The database migrator tool has been updated to handle this
+ change.
+
diff --git a/src/site/xdoc/doc_rest_operations.xml b/src/site/xdoc/doc_rest_operations.xml
index b4bc6c8c845..65a53d36d62 100644
--- a/src/site/xdoc/doc_rest_operations.xml
+++ b/src/site/xdoc/doc_rest_operations.xml
@@ -1839,6 +1839,28 @@ If-Match: W/"3"]]>
+
+
+
+ For some operations you may wish to bypass the HAPI FHIR
+ standard request parsing and/or response generation. In this
+ case you may use the manualRequest()
and
+ manualResponse()
attributes on the @Operation
+ annotation.
+
+
+ The following example shows an operation that parses the
+ request and generates a response (by echoing back the request).
+
+
+
+
+
+
+
+
+