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> 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 theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - boolean theIdempotent, String theOperationName, Class theOperationType, - OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { + boolean theIdempotent, String theOperationName, Class 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 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). +

+ + + + + + +
+