Add binary storage for Attachments (#1386)

* Have this working now

* Add utility method

* Build fixes

* Add changelog
This commit is contained in:
James Agnew 2019-07-17 18:31:17 -04:00 committed by GitHub
parent c73aebe0a0
commit 55c0d5b604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 1899 additions and 512 deletions

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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<T> extends BaseIdentifiableElement implement
public void writeExternal(ObjectOutput theOut) throws IOException {
theOut.writeObject(getValueAsString());
}
@Override
public boolean hasValue() {
return !StringUtils.isBlank(getValueAsString());
}
}

View File

@ -1,25 +0,0 @@
package ca.uhn.fhir.model.api;
/*
* #%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%
*/
public interface IPrimitiveDatatypeWithPrecision<T, P> extends IPrimitiveDatatype<T> {
}

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.model.api;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.io.IOException;
import java.io.Writer;
public interface IStreamingDatatype<T> extends IPrimitiveType<T> {
void writeAsText(Writer theWriter) throws IOException;
}

View File

@ -87,4 +87,37 @@ public @interface Operation {
*/
BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION;
/**
* If this is set to <code>true</code> (default is <code>false</code> and this is almost
* always the right choice), the framework will not attempt to generate a response to
* this method.
* <p>
* 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
* <code>@Operation</code> method.
* </p>
* <p>
* Note that this will mean that interceptor methods will not get fired for the
* response, so there are security implications to using this flag.
* </p>
*/
boolean manualResponse() default false;
/**
* If this is set to <code>true</code> (default is <code>false</code> 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 <code>@Operation</code> method.
* <p>
* This is useful if you want to include an {@link javax.servlet.http.HttpServletRequest}
* in your method parameters and parse the request yourself.
* </p>
*/
boolean manualRequest() default false;
/**
* If this is set to <code>true</code>, this method will be a <b>global operation</b>
* meaning that it applies to all resource types
*/
boolean global() default false;
}

View File

@ -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);

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.util;
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<byte[]> getOrCreateData(FhirContext theContext, ICompositeType theAttachment) {
BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "data");
List<IBase> entries = entryChild.getAccessor().getValues(theAttachment);
return entries
.stream()
.map(t -> (IPrimitiveType<byte[]>) t)
.findFirst()
.orElseGet(() -> {
IPrimitiveType<byte[]> binary = newPrimitive(theContext, "base64Binary", null);
entryChild.getMutator().setValue(theAttachment, binary);
return binary;
});
}
@SuppressWarnings("unchecked")
public static IPrimitiveType<String> getOrCreateContentType(FhirContext theContext, ICompositeType theAttachment) {
BaseRuntimeChildDefinition entryChild = getChild(theContext, theAttachment, "contentType");
List<IBase> entries = entryChild.getAccessor().getValues(theAttachment);
return entries
.stream()
.map(t -> (IPrimitiveType<String>) t)
.findFirst()
.orElseGet(() -> {
IPrimitiveType<String> 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 <T> IPrimitiveType<T> newPrimitive(FhirContext theContext, String theType, T theValue) {
IPrimitiveType<T> primitive = (IPrimitiveType<T>) 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);
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
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<SoftReference<Map<String, SimpleDateFormat>>> 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<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
Map<String, SimpleDateFormat> 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> 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<SoftReference<Map<String, SimpleDateFormat>>>
THREADLOCAL_FORMATS = new ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>() {
@Override
protected SoftReference<Map<String, SimpleDateFormat>> initialValue() {
return new SoftReference<Map<String, SimpleDateFormat>>(
new HashMap<String, SimpleDateFormat>());
}
};
/**
* 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<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
Map<String, SimpleDateFormat> formats = ref.get();
if (formats == null) {
formats = new HashMap<String, SimpleDateFormat>();
THREADLOCAL_FORMATS.set(
new SoftReference<Map<String, SimpleDateFormat>>(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> T notNull(final T argument, final String name) {
if (argument == null) {
throw new IllegalArgumentException(name + " may not be null");
}
return argument;
}
}

View File

@ -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 &quot; and
* &lt; 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 &quot; and
* &lt; 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;
}
}
}

View File

@ -29,6 +29,8 @@ public interface IPrimitiveType<T> extends IBaseDatatype {
String getValueAsString();
T getValue();
boolean hasValue();
IPrimitiveType<T> setValue(T theValue) throws IllegalArgumentException;

View File

@ -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}

View File

@ -0,0 +1,46 @@
package ca.uhn.fhir.jpa.binstore;
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);
}
}

View File

@ -0,0 +1,146 @@
package ca.uhn.fhir.jpa.binstore;
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());
}
}
}

View File

@ -0,0 +1,117 @@
package ca.uhn.fhir.jpa.binstore;
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 <code>true</code> 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;
}
}
}

View File

@ -0,0 +1,62 @@
package ca.uhn.fhir.jpa.binstore;
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<String, byte[]> myDataMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, StoredDetails> 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();
}
}

View File

@ -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();

View File

@ -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;

View File

@ -561,7 +561,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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<T extends IBaseResource> extends B
}
}
}
}
@Override

View File

@ -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

View File

@ -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;
@ -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() {

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,237 @@
package ca.uhn.fhir.jpa.provider;
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 <code>$binary-access-read</code> and <code>$binary-access-write</code>
* 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<String> thePath,
ServletRequestDetails theRequestDetails,
HttpServletRequest theServletRequest,
HttpServletResponse theServletResponse) throws IOException {
validateResourceTypeAndPath(theResourceId, thePath);
IFhirResourceDao dao = getDaoForRequest(theResourceId);
IBaseResource resource = dao.read(theResourceId, theRequestDetails, false);
ICompositeType attachment = findAttachmentForRequest(resource, thePath, theRequestDetails);
IBaseHasExtensions attachmentHasExt = (IBaseHasExtensions) attachment;
Optional<? extends IBaseExtension<?, ?>> attachmentId = attachmentHasExt
.getExtension()
.stream()
.filter(t -> JpaConstants.EXT_ATTACHMENT_EXTERNAL_BINARY_ID.equals(t.getUrl()))
.findFirst();
if (attachmentId.isPresent()) {
@SuppressWarnings("unchecked")
IPrimitiveType<String> value = (IPrimitiveType<String>) 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<String> contentTypeDt = AttachmentUtil.getOrCreateContentType(theRequestDetails.getFhirContext(), attachment);
String contentType = contentTypeDt.getValueAsString();
contentType = StringUtils.defaultIfBlank(contentType, Constants.CT_OCTET_STREAM);
IPrimitiveType<byte[]> 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<String> 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<String> blobIdString = (IPrimitiveType<String>) 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<String> thePath, ServletRequestDetails theRequestDetails) {
FhirContext ctx = theRequestDetails.getFhirContext();
String path = thePath.getValueAsString();
Optional<ICompositeType> 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<String> 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;
}
}

View File

@ -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<T extends IResource> extends BaseJpaResourceProvider<T> {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/*

View File

@ -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;

View File

@ -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.*;
/*

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<T extends IAnyResource> extends BaseJpaResourceProvider<T> {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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.*;
/*

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<T extends IAnyResource> extends BaseJpaResourceProvider<T> {

View File

@ -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;

View File

@ -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();

View File

@ -0,0 +1,24 @@
package ca.uhn.fhir.jpa.util;
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<Date> {
@Override
public Date deserialize(JsonParser theParser, DeserializationContext theDeserializationContext) throws IOException {
String string = theParser.getValueAsString();
if (isNotBlank(string)) {
return new DateTimeType(string).getValue();
}
return null;
}
}

View File

@ -0,0 +1,20 @@
package ca.uhn.fhir.jpa.util;
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<Date> {
@Override
public void serialize(Date theValue, JsonGenerator theGen, SerializerProvider theSerializers) throws IOException {
if (theValue != null) {
theGen.writeString(new InstantType(theValue).getValueAsString());
}
}
}

View File

@ -25,10 +25,12 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
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.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
@ -43,6 +45,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class TestUtil {
private static final int MAX_LENGTH = 30;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
public static final int MAX_COL_LENGTH = 2000;
/**
* non instantiable
@ -154,6 +157,42 @@ public class TestUtil {
if (column != null) {
assertNotADuplicateName(column.name(), 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);
@ -209,4 +248,7 @@ public class TestUtil {
public static void sleepOneClick() {
sleepAtLeast(1);
}
}

View File

@ -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}$"));
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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<AllergyIntolerance> myAllergyIntoleranceDao;
@Autowired
protected BinaryAccessProvider myBinaryAccessProvider;
@Autowired
protected ApplicationContext myAppCtx;
@Autowired
@Qualifier("myAppointmentDaoR4")

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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.*;

View File

@ -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());

View File

@ -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<Coding> 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<Coding> 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<Coding> 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<Coding> 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());
}

View File

@ -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);

View File

@ -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;

View File

@ -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()

View File

@ -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")

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.util;
package ca.uhn.fhir.jpa.model.util;
/*-
* #%L
@ -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";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
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.
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
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.
* <p>
* Note that if the resource is now deleted, this may cause
* the delivery to be cancelled altogether.
* </p>
*
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
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";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
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";
}

View File

@ -61,7 +61,11 @@ public class Retrier<T> {
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> 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);

View File

@ -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<S extends IBaseResource> {
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<S extends IBaseResource> {
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<S extends IBaseResource> {
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<S extends IBaseResource> {
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<S extends IBaseResource> {
} 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) {

View File

@ -24,57 +24,6 @@ import org.hl7.fhir.dstu2.model.Subscription;
public class SubscriptionConstants {
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
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.
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
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.
* <p>
* Note that if the resource is now deleted, this may cause
* the delivery to be cancelled altogether.
* </p>
*
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
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

View File

@ -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;

View File

@ -1048,7 +1048,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
/*
* If it's a 410 Gone, we want to include a location header inthe response
* If it's a 410 Gone, we want to include a location header in the response
* if we can, since that can include the resource version which is nice
* for the user.
*/

View File

@ -300,8 +300,12 @@ public class ConsentInterceptor {
}
private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
return Boolean.TRUE.equals(authorizedObj);
boolean retVal = false;
if (theRequestDetails != null) {
Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
retVal = Boolean.TRUE.equals(authorizedObj);
}
return retVal;
}
@SuppressWarnings("unchecked")

View File

@ -89,6 +89,8 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
} else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
} else if (void.class.equals(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.VOID;
} else {
throw new ConfigurationException(
"Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
@ -238,9 +240,10 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
public IBaseResource doInvokeServer(IRestfulServer<?> 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<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
@ -407,6 +413,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
BUNDLE_RESOURCE,
LIST_OF_RESOURCES,
METHOD_OUTCOME,
VOID,
RESOURCE
}

View File

@ -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<ReturnType> myReturnParams;
private boolean myManualRequestMode;
private boolean myManualResponseMode;
protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
super(theReturnResourceType, theMethod, theContext, theProvider);
myBundleType = theBundleType;
@ -89,7 +92,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (isBlank(theOperationName)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
+ " but this annotation has no name defined");
+ " but this annotation has no name defined");
}
if (theOperationName.startsWith("$") == false) {
theOperationName = "$" + theOperationName;
@ -99,10 +102,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (theReturnTypeFromRp != null) {
setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
} else if (Modifier.isAbstract(theOperationType.getModifiers()) == false) {
setResourceName(theContext.getResourceDefinition(theOperationType).getName());
} else {
setResourceName(null);
}
setResourceName(theContext.getResourceDefinition(theOperationType).getName());
} else {
setResourceName(null);
}
if (theMethod.getReturnType().equals(IBundleProvider.class)) {
myReturnType = ReturnTypeEnum.BUNDLE;
@ -110,24 +113,24 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
myReturnType = ReturnTypeEnum.RESOURCE;
}
myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
if (getResourceName() == null) {
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
myCanOperateAtServerLevel = true;
if (myIdParamIndex != null) {
myCanOperateAtInstanceLevel = true;
}
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
myCanOperateAtServerLevel = true;
if (myIdParamIndex != null) {
myCanOperateAtInstanceLevel = true;
}
} else if (myIdParamIndex == null) {
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
myCanOperateAtTypeLevel = true;
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
myCanOperateAtTypeLevel = true;
} else {
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
myCanOperateAtInstanceLevel = true;
for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
if (next instanceof IdParam) {
myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
}
}
myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
myCanOperateAtInstanceLevel = true;
for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
if (next instanceof IdParam) {
myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
}
}
}
myReturnParams = new ArrayList<>();
@ -151,10 +154,23 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
/**
* Constructor - This is the constructor that is called when binding a
* standard @Operation method.
*/
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
Operation theAnnotation) {
Operation theAnnotation) {
this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(),
theAnnotation.bundleType());
theAnnotation.bundleType());
myManualRequestMode = theAnnotation.manualRequest();
myManualResponseMode = theAnnotation.manualResponse();
myGlobal = theAnnotation.global();
}
@Override
public boolean isGlobalMethod() {
return myGlobal;
}
public String getDescription() {
@ -206,7 +222,9 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) {
return false;
if (!isGlobalMethod()) {
return false;
}
}
}
@ -222,12 +240,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
boolean requestHasId = theRequest.getId() != null;
if (requestHasId) {
return myCanOperateAtInstanceLevel;
}
if (isNotBlank(theRequest.getResourceName())) {
return myCanOperateAtTypeLevel;
}
return myCanOperateAtServerLevel;
return myCanOperateAtInstanceLevel;
}
if (isNotBlank(theRequest.getResourceName())) {
return myCanOperateAtTypeLevel;
}
return myCanOperateAtServerLevel;
}
@Override
@ -240,6 +258,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) {
retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
} else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) {
retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
}
return retVal;
}
@ -256,7 +280,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
@Override
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
if (theRequest.getRequestType() == RequestTypeEnum.POST) {
if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) {
IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
}
@ -286,6 +310,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
if (myManualResponseMode) {
return null;
}
IBundleProvider retVal = toResourceList(response);
return retVal;
}
@ -316,6 +344,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
public boolean isManualRequestMode() {
return myManualRequestMode;
}
public static class ReturnType {
private int myMax;
private int myMin;

View File

@ -210,7 +210,9 @@ public class OperationParameter implements IParameter {
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
List<Object> matchingParamValues = new ArrayList<Object>();
if (theRequest.getRequestType() == RequestTypeEnum.GET) {
OperationMethodBinding method = (OperationMethodBinding) theMethodBinding;
if (theRequest.getRequestType() == RequestTypeEnum.GET || method.isManualRequestMode()) {
translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
} else {
translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);

View File

@ -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("<Bundle"));
}
assertEquals("$OP_PLAIN_PROVIDER_ON_INSTANCE", ourLastMethod);
assertEquals("Patient/123", ourLastId.toUnqualifiedVersionless().getValue());
assertEquals(RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE, ourLastRestOperation);
}
@Test
public void testInstanceEverythingHapiClient() {
ourCtx.newRestfulGenericClient("http://localhost:" + ourPort).operation().onInstance(new IdType("Patient/123")).named("$everything").withParameters(new Parameters()).execute();
@ -226,6 +272,46 @@ public class OperationServerR4Test {
}
@Test
public void testManualInputAndOutput() throws Exception {
byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1};
ContentType contentType = ContentType.IMAGE_PNG;
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutput");
httpPost.setEntity(new ByteArrayEntity(bytes, contentType));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
String receivedContentType = status.getEntity().getContentType().getValue();
byte[] receivedBytes = IOUtils.toByteArray(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(contentType.getMimeType(), receivedContentType);
assertArrayEquals(bytes, receivedBytes);
}
}
@Test
public void testManualInputAndOutputWithUrlParam() throws Exception {
byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1};
ContentType contentType = ContentType.IMAGE_PNG;
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutputWithParam?param1=value");
httpPost.setEntity(new ByteArrayEntity(bytes, contentType));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
String receivedContentType = status.getEntity().getContentType().getValue();
byte[] receivedBytes = IOUtils.toByteArray(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(contentType.getMimeType(), receivedContentType);
assertArrayEquals(bytes, receivedBytes);
assertEquals("value", ourLastParam1.getValue());
}
}
@Test
public void testOperationCantUseGetIfItIsntIdempotent() throws Exception {
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
@ -439,7 +525,7 @@ public class OperationServerR4Test {
@Test
public void testOperationWithBundleProviderResponse() throws Exception {
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/$OP_INSTANCE_BUNDLE_PROVIDER?_pretty=true");
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/$OP_SERVER_BUNDLE_PROVIDER?_pretty=true");
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
@ -480,7 +566,6 @@ public class OperationServerR4Test {
assertThat(response, containsString("Can not invoke operation $OP_TYPE using HTTP GET because parameter PARAM2 is not a primitive datatype"));
}
@Test
public void testOperationWithListParam() throws Exception {
Parameters p = new Parameters();
@ -618,7 +703,6 @@ public class OperationServerR4Test {
}
}
private static IBaseResource ourNextResponse;
public static class PatientProvider implements IResourceProvider {
@ -750,6 +834,36 @@ public class OperationServerR4Test {
return new Bundle();
}
@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();
}
@Operation(name="$manualInputAndOutputWithParam", manualResponse=true, manualRequest=true)
public void manualInputAndOutputWithParam(
@OperationParam(name="param1") StringType theParam1,
HttpServletRequest theServletRequest,
HttpServletResponse theServletResponse
) throws IOException {
ourLastParam1 = theParam1;
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();
}
/**
* Just to make sure this method doesn't "steal" calls
*/
@ -762,12 +876,16 @@ public class OperationServerR4Test {
}
}
private static RestOperationTypeEnum ourLastRestOperation;
public static class PlainProvider {
@Operation(name = "$OP_INSTANCE_BUNDLE_PROVIDER", idempotent = true)
public IBundleProvider opInstanceReturnsBundleProvider() {
ourLastMethod = "$OP_INSTANCE_BUNDLE_PROVIDER";
@Operation(name = "$OP_PLAIN_PROVIDER_ON_INSTANCE", idempotent = true, global = true)
public IBundleProvider opPlainProviderOnInstance(@IdParam IdType theId, RequestDetails theRequestDetails) {
ourLastMethod = "$OP_PLAIN_PROVIDER_ON_INSTANCE";
ourLastId = theId;
ourLastRestOperation = theRequestDetails.getRestOperationType();
List<IBaseResource> resources = new ArrayList<IBaseResource>();
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<IBaseResource> resources = new ArrayList<IBaseResource>();
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<String> 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();

View File

@ -296,6 +296,26 @@
<action type="add">
Expanded ValueSets now populate <![CDATA[<code>ValueSet.expansion.contains.designation.language</code>]]>.
</action>
<action type="add">
@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.
</action>
<action type="add">
@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.
</action>
<action type="add">
A new resource provider for JPA servers called
<![CDATA[<code>BinaryAccessProvider</code>]]>
has been added. This provider serves two custom operations called
<![CDATA[<code>$binary-access-read</code>]]> and
<![CDATA[<code>$binary-access-write</code>]]> that can be used to
request binary data in Attachments as raw binary content instead of
as base 64 encoded content.
</action>
</release>
<release version="3.8.0" date="2019-05-30" description="Hippo">
<action type="fix">

View File

@ -1839,6 +1839,28 @@ If-Match: W/"3"]]></pre>
</p>
</subsection>
<subsection name="Manually handing Request/Response">
<p>
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 <code>manualRequest()</code> and
<code>manualResponse()</code> attributes on the <code>@Operation</code>
annotation.
</p>
<p>
The following example shows an operation that parses the
request and generates a response (by echoing back the request).
</p>
<macro name="snippet">
<param name="id" value="manualInputAndOutput" />
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
</subsection>
</section>
</body>