Merge remote-tracking branch 'upstream/master' and fix tinder-test

conflict
This commit is contained in:
bdenton 2018-11-29 09:45:00 -08:00
commit 689340368e
235 changed files with 9349 additions and 4105 deletions

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -10,7 +10,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -10,7 +10,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -10,7 +10,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-standalone-overlay-example</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -73,12 +73,12 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseRuntimeElementCompositeDefinition.class);
private Map<String, Integer> forcedOrder = null;
private List<BaseRuntimeChildDefinition> myChildren = new ArrayList<BaseRuntimeChildDefinition>();
private List<BaseRuntimeChildDefinition> myChildren = new ArrayList<>();
private List<BaseRuntimeChildDefinition> myChildrenAndExtensions;
private Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinitions;
private FhirContext myContext;
private Map<String, BaseRuntimeChildDefinition> myNameToChild = new HashMap<String, BaseRuntimeChildDefinition>();
private List<ScannedField> myScannedFields = new ArrayList<BaseRuntimeElementCompositeDefinition.ScannedField>();
private final FhirContext myContext;
private Map<String, BaseRuntimeChildDefinition> myNameToChild = new HashMap<>();
private List<ScannedField> myScannedFields = new ArrayList<>();
private volatile boolean mySealed;
@SuppressWarnings("unchecked")
@ -92,12 +92,12 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
* We scan classes for annotated fields in the class but also all of its superclasses
*/
Class<? extends IBase> current = theImplementingClass;
LinkedList<Class<? extends IBase>> classes = new LinkedList<Class<? extends IBase>>();
LinkedList<Class<? extends IBase>> classes = new LinkedList<>();
do {
if (forcedOrder == null) {
ChildOrder childOrder = current.getAnnotation(ChildOrder.class);
if (childOrder != null) {
forcedOrder = new HashMap<String, Integer>();
forcedOrder = new HashMap<>();
for (int i = 0; i < childOrder.names().length; i++) {
String nextName = childOrder.names()[i];
if (nextName.endsWith("[x]")) {
@ -115,7 +115,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
}
} while (current != null);
Set<Field> fields = new HashSet<Field>();
Set<Field> fields = new HashSet<>();
for (Class<? extends IBase> nextClass : classes) {
int fieldIndexInClass = 0;
for (Field next : nextClass.getDeclaredFields()) {
@ -192,9 +192,9 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
}
private void scanCompositeElementForChildren() {
Set<String> elementNames = new HashSet<String>();
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> orderToElementDef = new TreeMap<Integer, BaseRuntimeDeclaredChildDefinition>();
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> orderToExtensionDef = new TreeMap<Integer, BaseRuntimeDeclaredChildDefinition>();
Set<String> elementNames = new HashSet<>();
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> orderToElementDef = new TreeMap<>();
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> orderToExtensionDef = new TreeMap<>();
scanCompositeElementForChildren(elementNames, orderToElementDef, orderToExtensionDef);
@ -203,7 +203,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
* Find out how many elements don't match any entry in the list
* for forced order. Those elements come first.
*/
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> newOrderToExtensionDef = new TreeMap<Integer, BaseRuntimeDeclaredChildDefinition>();
TreeMap<Integer, BaseRuntimeDeclaredChildDefinition> newOrderToExtensionDef = new TreeMap<>();
int unknownCount = 0;
for (BaseRuntimeDeclaredChildDefinition nextEntry : orderToElementDef.values()) {
if (!forcedOrder.containsKey(nextEntry.getElementName())) {
@ -220,7 +220,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
orderToElementDef = newOrderToExtensionDef;
}
TreeSet<Integer> orders = new TreeSet<Integer>();
TreeSet<Integer> orders = new TreeSet<>();
orders.addAll(orderToElementDef.keySet());
orders.addAll(orderToExtensionDef.keySet());
@ -329,7 +329,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
* Anything that's marked as unknown is given a new ID that is <0 so that it doesn't conflict with any given IDs and can be figured out later
*/
if (order == Child.ORDER_UNKNOWN) {
order = Integer.valueOf(0);
order = 0;
while (orderMap.containsKey(order)) {
order++;
}
@ -386,7 +386,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
/*
* Child is a resource reference
*/
List<Class<? extends IBaseResource>> refTypesList = new ArrayList<Class<? extends IBaseResource>>();
List<Class<? extends IBaseResource>> refTypesList = new ArrayList<>();
for (Class<? extends IElement> nextType : childAnnotation.type()) {
if (IBaseReference.class.isAssignableFrom(nextType)) {
refTypesList.add(myContext.getVersion().getVersion().isRi() ? IAnyResource.class : IResource.class);
@ -469,10 +469,10 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
next.sealAndInitialize(theContext, theClassToElementDefinitions);
}
myNameToChild = new HashMap<String, BaseRuntimeChildDefinition>();
myNameToChild = new HashMap<>();
for (BaseRuntimeChildDefinition next : myChildren) {
if (next instanceof RuntimeChildChoiceDefinition) {
String key = ((RuntimeChildChoiceDefinition) next).getElementName()+"[x]";
String key = next.getElementName()+"[x]";
myNameToChild.put(key, next);
}
for (String nextName : next.getValidChildNames()) {
@ -486,7 +486,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
myChildren = Collections.unmodifiableList(myChildren);
myNameToChild = Collections.unmodifiableMap(myNameToChild);
List<BaseRuntimeChildDefinition> children = new ArrayList<BaseRuntimeChildDefinition>();
List<BaseRuntimeChildDefinition> children = new ArrayList<>();
children.addAll(myChildren);
/*
@ -554,11 +554,11 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
private static class ScannedField {
private Child myChildAnnotation;
private List<Class<? extends IBase>> myChoiceTypes = new ArrayList<Class<? extends IBase>>();
private List<Class<? extends IBase>> myChoiceTypes = new ArrayList<>();
private Class<?> myElementType;
private Field myField;
private boolean myFirstFieldInNewClass;
public ScannedField(Field theField, Class<?> theClass, boolean theFirstFieldInNewClass) {
ScannedField(Field theField, Class<?> theClass, boolean theFirstFieldInNewClass) {
myField = theField;
myFirstFieldInNewClass = theFirstFieldInNewClass;
@ -575,9 +575,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
myChildAnnotation = childAnnotation;
myElementType = ModelScanner.determineElementType(theField);
for (Class<? extends IBase> nextChoiceType : childAnnotation.type()) {
myChoiceTypes.add(nextChoiceType);
}
Collections.addAll(myChoiceTypes, childAnnotation.type());
}
public Child getChildAnnotation() {

View File

@ -19,17 +19,6 @@ package ca.uhn.fhir.context;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.RuntimeSearchParam.RuntimeSearchParamStatusEnum;
import ca.uhn.fhir.model.api.*;
@ -38,6 +27,19 @@ import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.XhtmlDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.util.ReflectionUtil;
import org.hl7.fhir.instance.model.api.*;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.Map.Entry;
import static org.apache.commons.lang3.StringUtils.isBlank;
class ModelScanner {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ModelScanner.class);
@ -67,32 +69,6 @@ class ModelScanner {
init(theExistingDefinitions, toScan);
}
static Class<?> determineElementType(Field next) {
Class<?> nextElementType = next.getType();
if (List.class.equals(nextElementType)) {
nextElementType = ReflectionUtil.getGenericCollectionTypeOfField(next);
} else if (Collection.class.isAssignableFrom(nextElementType)) {
throw new ConfigurationException("Field '" + next.getName() + "' in type '" + next.getClass().getCanonicalName() + "' is a Collection - Only java.util.List curently supported");
}
return nextElementType;
}
@SuppressWarnings("unchecked")
static IValueSetEnumBinder<Enum<?>> getBoundCodeBinder(Field theNext) {
Class<?> bound = getGenericCollectionTypeOfCodedField(theNext);
if (bound == null) {
throw new ConfigurationException("Field '" + theNext + "' has no parameter for " + BoundCodeDt.class.getSimpleName() + " to determine enum type");
}
String fieldName = "VALUESET_BINDER";
try {
Field bindingField = bound.getField(fieldName);
return (IValueSetEnumBinder<Enum<?>>) bindingField.get(null);
} catch (Exception e) {
throw new ConfigurationException("Field '" + theNext + "' has type parameter " + bound.getCanonicalName() + " but this class has no valueset binding field (must have a field called " + fieldName + ")", e);
}
}
public Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> getClassToElementDefinitions() {
return myClassToElementDefinitions;
}
@ -137,11 +113,7 @@ class ModelScanner {
for (Class<? extends IBase> nextClass : typesToScan) {
scan(nextClass);
}
for (Iterator<Class<? extends IBase>> iter = myScanAlso.iterator(); iter.hasNext();) {
if (myClassToElementDefinitions.containsKey(iter.next())) {
iter.remove();
}
}
myScanAlso.removeIf(theClass -> myClassToElementDefinitions.containsKey(theClass));
typesToScan.clear();
typesToScan.addAll(myScanAlso);
myScanAlso.clear();
@ -177,16 +149,6 @@ class ModelScanner {
return retVal;
}
/**
* There are two implementations of all of the annotations (e.g. {@link Child} since the HL7.org ones will eventually replace the HAPI
* ones. Annotations can't extend each other or implement interfaces or anything like that, so rather than duplicate all of the annotation processing code this method just creates an interface
* Proxy to simulate the HAPI annotations if the HL7.org ones are found instead.
*/
static <T extends Annotation> T pullAnnotation(AnnotatedElement theTarget, Class<T> theAnnotationType) {
T retVal = theTarget.getAnnotation(theAnnotationType);
return retVal;
}
private void scan(Class<? extends IBase> theClass) throws ConfigurationException {
BaseRuntimeElementDefinition<?> existingDef = myClassToElementDefinitions.get(theClass);
if (existingDef != null) {
@ -246,7 +208,16 @@ class ModelScanner {
throw new ConfigurationException("Block type @" + Block.class.getSimpleName() + " annotation contains no name: " + theClass.getCanonicalName());
}
// Just in case someone messes up when upgrading from DSTU2
if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
if (BaseIdentifiableElement.class.isAssignableFrom(theClass)) {
throw new ConfigurationException("@Block class for version " + myContext.getVersion().getVersion().name() + " should not extend " + BaseIdentifiableElement.class.getSimpleName() + ": " + theClass.getName());
}
}
RuntimeResourceBlockDefinition blockDef = new RuntimeResourceBlockDefinition(resourceName, theClass, isStandardType(theClass), myContext, myClassToElementDefinitions);
blockDef.populateScanAlso(myScanAlso);
myClassToElementDefinitions.put(theClass, blockDef);
}
@ -272,14 +243,6 @@ class ModelScanner {
elementDef.populateScanAlso(myScanAlso);
}
static Class<? extends Enum<?>> determineEnumTypeForBoundField(Field next) {
@SuppressWarnings("unchecked")
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next);
return enumType;
}
private String scanPrimitiveDatatype(Class<? extends IPrimitiveType<?>> theClass, DatatypeDef theDatatypeDefinition) {
ourLog.debug("Scanning resource class: {}", theClass.getName());
@ -466,6 +429,48 @@ class ModelScanner {
return retVal;
}
static Class<?> determineElementType(Field next) {
Class<?> nextElementType = next.getType();
if (List.class.equals(nextElementType)) {
nextElementType = ReflectionUtil.getGenericCollectionTypeOfField(next);
} else if (Collection.class.isAssignableFrom(nextElementType)) {
throw new ConfigurationException("Field '" + next.getName() + "' in type '" + next.getClass().getCanonicalName() + "' is a Collection - Only java.util.List curently supported");
}
return nextElementType;
}
@SuppressWarnings("unchecked")
static IValueSetEnumBinder<Enum<?>> getBoundCodeBinder(Field theNext) {
Class<?> bound = getGenericCollectionTypeOfCodedField(theNext);
if (bound == null) {
throw new ConfigurationException("Field '" + theNext + "' has no parameter for " + BoundCodeDt.class.getSimpleName() + " to determine enum type");
}
String fieldName = "VALUESET_BINDER";
try {
Field bindingField = bound.getField(fieldName);
return (IValueSetEnumBinder<Enum<?>>) bindingField.get(null);
} catch (Exception e) {
throw new ConfigurationException("Field '" + theNext + "' has type parameter " + bound.getCanonicalName() + " but this class has no valueset binding field (must have a field called " + fieldName + ")", e);
}
}
/**
* There are two implementations of all of the annotations (e.g. {@link Child} since the HL7.org ones will eventually replace the HAPI
* ones. Annotations can't extend each other or implement interfaces or anything like that, so rather than duplicate all of the annotation processing code this method just creates an interface
* Proxy to simulate the HAPI annotations if the HL7.org ones are found instead.
*/
static <T extends Annotation> T pullAnnotation(AnnotatedElement theTarget, Class<T> theAnnotationType) {
T retVal = theTarget.getAnnotation(theAnnotationType);
return retVal;
}
static Class<? extends Enum<?>> determineEnumTypeForBoundField(Field next) {
@SuppressWarnings("unchecked")
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next);
return enumType;
}
private static Class<?> getGenericCollectionTypeOfCodedField(Field next) {
Class<?> type;
ParameterizedType collectionType = (ParameterizedType) next.getGenericType();

View File

@ -149,9 +149,9 @@ public class RuntimeChildUndeclaredExtensionDefinition extends BaseRuntimeChildD
@Override
void sealAndInitialize(FhirContext theContext, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
Map<String, BaseRuntimeElementDefinition<?>> datatypeAttributeNameToDefinition = new HashMap<String, BaseRuntimeElementDefinition<?>>();
myDatatypeToAttributeName = new HashMap<Class<? extends IBase>, String>();
myDatatypeToDefinition = new HashMap<Class<? extends IBase>, BaseRuntimeElementDefinition<?>>();
Map<String, BaseRuntimeElementDefinition<?>> datatypeAttributeNameToDefinition = new HashMap<>();
myDatatypeToAttributeName = new HashMap<>();
myDatatypeToDefinition = new HashMap<>();
for (BaseRuntimeElementDefinition<?> next : theClassToElementDefinitions.values()) {
if (next instanceof IRuntimeDatatypeDefinition) {

View File

@ -60,7 +60,7 @@ public interface IQueryParameterType extends Serializable {
public String getValueAsQueryToken(FhirContext theContext);
/**
* This method will return any qualifier that should be appended to the parameter name (e.g ":exact")
* This method will return any qualifier that should be appended to the parameter name (e.g ":exact"). Returns null if none are present.
*/
public String getQueryParameterQualifier();

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
@ -623,6 +624,16 @@ public abstract class BaseParser implements IParser {
return mySuppressNarratives;
}
@Override
public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException {
return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8));
}
@Override
public <T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException {
return parseResource(theResourceType, new InputStreamReader(theInputStream, Charsets.UTF_8));
}
@Override
public <T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException {

View File

@ -19,14 +19,23 @@ package ca.uhn.fhir.parser;
* limitations under the License.
* #L%
*/
import java.io.*;
import java.util.*;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.api.EncodingEnum;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* A parser, which can be used to convert between HAPI FHIR model/structure objects, and their respective String wire
@ -127,6 +136,20 @@ public interface IParser {
*/
<T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException;
/**
* Parses a resource
*
* @param theResourceType
* The resource type to use. This can be used to explicitly specify a class which extends a built-in type
* (e.g. a custom type extending the default Patient class)
* @param theInputStream
* The InputStream to parse input from, <b>with an implied charset of UTF-8</b>. Note that the InputStream will not be closed by the parser upon completion.
* @return A parsed resource
* @throws DataFormatException
* If the resource can not be parsed because the data is not recognized or invalid for any reason
*/
<T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException;
/**
* Parses a resource
*
@ -153,6 +176,19 @@ public interface IParser {
*/
IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException;
/**
* Parses a resource
*
* @param theInputStream
* The InputStream to parse input from (charset is assumed to be UTF-8).
* Note that the stream will not be closed by the parser upon completion.
* @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or
* {@link IAnyResource} depending on the specific FhirContext which created this parser.
* @throws DataFormatException
* If the resource can not be parsed because the data is not recognized or invalid for any reason
*/
IBaseResource parseResource(InputStream theInputStream) throws ConfigurationException, DataFormatException;
/**
* Parses a resource
*

View File

@ -42,8 +42,14 @@ public class Constants {
*/
public static final Set<String> CORS_ALLWED_METHODS;
public static final String CT_FHIR_JSON = "application/json+fhir";
/**
* The FHIR MimeType for JSON encoding in FHIR DSTU3+
*/
public static final String CT_FHIR_JSON_NEW = "application/fhir+json";
public static final String CT_FHIR_XML = "application/xml+fhir";
/**
* The FHIR MimeType for XML encoding in FHIR DSTU3+
*/
public static final String CT_FHIR_XML_NEW = "application/fhir+xml";
public static final String CT_HTML = "text/html";
public static final String CT_HTML_WITH_UTF8 = "text/html" + CHARSET_UTF8_CTSUFFIX;
@ -86,6 +92,7 @@ public class Constants {
public static final String HEADER_CONTENT_LOCATION = "Content-Location";
public static final String HEADER_CONTENT_LOCATION_LC = HEADER_CONTENT_LOCATION.toLowerCase();
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TYPE_LC = HEADER_CONTENT_TYPE.toLowerCase();
public static final String HEADER_COOKIE = "Cookie";
public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods";
public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

View File

@ -20,11 +20,13 @@ package ca.uhn.fhir.rest.api;
* #L%
*/
import ca.uhn.fhir.util.CoverageIgnore;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.util.CoverageIgnore;
import java.util.List;
import java.util.Map;
public class MethodOutcome {
@ -32,6 +34,7 @@ public class MethodOutcome {
private IIdType myId;
private IBaseOperationOutcome myOperationOutcome;
private IBaseResource myResource;
private Map<String, List<String>> myResponseHeaders;
/**
* Constructor
@ -43,11 +46,8 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* @param theId The ID of the created/updated resource
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
@CoverageIgnore
@ -59,11 +59,8 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theBaseOperationOutcome
* The operation outcome to return with the response (or null for none)
* @param theId The ID of the created/updated resource
* @param theBaseOperationOutcome The operation outcome to return with the response (or null for none)
*/
public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome) {
myId = theId;
@ -73,14 +70,9 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theBaseOperationOutcome
* The operation outcome to return with the response (or null for none)
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* @param theId The ID of the created/updated resource
* @param theBaseOperationOutcome The operation outcome to return with the response (or null for none)
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome, Boolean theCreated) {
@ -92,8 +84,7 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
* @param theId The ID of the created/updated resource
*/
public MethodOutcome(IIdType theId) {
myId = theId;
@ -102,8 +93,7 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theOperationOutcome
* The operation outcome resource to return
* @param theOperationOutcome The operation outcome resource to return
*/
public MethodOutcome(IBaseOperationOutcome theOperationOutcome) {
myOperationOutcome = theOperationOutcome;
@ -117,10 +107,35 @@ public class MethodOutcome {
return myCreated;
}
/**
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the
* result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* <p>
* Users of HAPI should only interact with this method in Server applications
* </p>
*
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setCreated(Boolean theCreated) {
myCreated = theCreated;
return this;
}
public IIdType getId() {
return myId;
}
/**
* @param theId The ID of the created/updated resource
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setId(IIdType theId) {
myId = theId;
return this;
}
/**
* Returns the {@link IBaseOperationOutcome} resource to return to the client or <code>null</code> if none.
*
@ -130,6 +145,16 @@ public class MethodOutcome {
return myOperationOutcome;
}
/**
* Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
*
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
myOperationOutcome = theBaseOperationOutcome;
return this;
}
/**
* <b>From a client response:</b> If the method returned an actual resource body (e.g. a create/update with
* "Prefer: return=representation") this field will be populated with the
@ -139,42 +164,6 @@ public class MethodOutcome {
return myResource;
}
/**
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the
* result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* <p>
* Users of HAPI should only interact with this method in Server applications
* </p>
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setCreated(Boolean theCreated) {
myCreated = theCreated;
return this;
}
/**
* @param theId
* The ID of the created/updated resource
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setId(IIdType theId) {
myId = theId;
return this;
}
/**
* Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
myOperationOutcome = theBaseOperationOutcome;
return this;
}
/**
* <b>In a server response</b>: This field may be populated in server code with the final resource for operations
* where a resource body is being created/updated. E.g. for an update method, this field could be populated with
@ -183,6 +172,7 @@ public class MethodOutcome {
* This field is optional, but if it is populated the server will return the resource body if requested to
* do so via the HTTP Prefer header.
* </p>
*
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setResource(IBaseResource theResource) {
@ -190,4 +180,23 @@ public class MethodOutcome {
return this;
}
/**
* Gets the headers for the HTTP response
*/
public Map<String, List<String>> getResponseHeaders() {
return myResponseHeaders;
}
/**
* Sets the headers for the HTTP response
*/
public void setResponseHeaders(Map<String, List<String>> theResponseHeaders) {
myResponseHeaders = theResponseHeaders;
}
public void setCreatedUsingStatusCode(int theResponseStatusCode) {
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
setCreated(true);
}
}
}

View File

@ -31,6 +31,7 @@ public interface IHttpRequest {
/**
* Add a header to the request
*
* @param theName the header name
* @param theValue the header value
*/
@ -38,6 +39,7 @@ public interface IHttpRequest {
/**
* Execute the request
*
* @return the response
*/
IHttpResponse execute() throws IOException;
@ -51,6 +53,7 @@ public interface IHttpRequest {
/**
* Return the request body as a string.
* If this is not supported by the underlying technology, null is returned
*
* @return a string representation of the request or null if not supported or empty.
*/
String getRequestBodyFromStream() throws IOException;
@ -65,4 +68,10 @@ public interface IHttpRequest {
*/
String getHttpVerbName();
/**
* Remove any headers matching the given name
*
* @param theHeaderName The header name, e.g. "Accept" (must not be null or blank)
*/
void removeHeaders(String theHeaderName);
}

View File

@ -70,7 +70,7 @@ public interface IHttpResponse {
void close();
/**
* Returna reader for the response entity
* Returns a reader for the response entity
*/
Reader createReader() throws IOException;

View File

@ -19,15 +19,18 @@ package ca.uhn.fhir.rest.client.exceptions;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException;
import java.io.Reader;
import org.apache.commons.io.IOUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.CoverageIgnore;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import static org.apache.commons.lang3.StringUtils.isBlank;
@CoverageIgnore
public class NonFhirResponseException extends BaseServerResponseException {
@ -37,23 +40,29 @@ public class NonFhirResponseException extends BaseServerResponseException {
/**
* Constructor
*
* @param theMessage
* The message
* @param theResponseText
* @param theStatusCode
* @param theResponseReader
* @param theContentType
* @param theMessage The message
* @param theStatusCode The HTTP status code
*/
NonFhirResponseException(int theStatusCode, String theMessage) {
super(theStatusCode, theMessage);
}
public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, InputStream theInputStream) {
return newInstance(theStatusCode, theContentType, new InputStreamReader(theInputStream, Charsets.UTF_8));
}
public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, Reader theReader) {
String responseBody = "";
try {
responseBody = IOUtils.toString(theReader);
} catch (IOException e) {
IOUtils.closeQuietly(theReader);
// ignore
} finally {
try {
theReader.close();
} catch (IOException theE) {
// ignore
}
}
NonFhirResponseException retVal;

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -52,10 +53,39 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
*/
T elementsSubset(String... theElements);
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
*/
T encoded(EncodingEnum theEncoding);
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
* @see #encoded(EncodingEnum)
*/
T encodedJson();
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
* @see #encoded(EncodingEnum)
*/
T encodedXml();
/**
@ -84,6 +114,9 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
*/
T preferResponseTypes(List<Class<? extends IBaseResource>> theTypes);
/**
* Request pretty-printed response via the <code>_pretty</code> parameter
*/
T prettyPrint();
/**
@ -91,4 +124,23 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
*/
T summaryMode(SummaryEnum theSummary);
/**
* Specifies a custom <code>Accept</code> header that should be supplied with the
* request.
* <p>
* Note that this method overrides any encoding preferences specified with
* {@link #encodedJson()} or {@link #encodedXml()}. It is generally easier to
* just use those methods if you simply want to request a specific FHIR encoding.
* </p>
*
* @param theHeaderValue The header value, e.g. "application/fhir+json". Constants such
* as {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_XML_NEW} and
* {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_JSON_NEW} may
* be useful. If set to <code>null</code> or an empty string, the
* default Accept header will be used.
* @see #encoded(EncodingEnum)
* @see #encodedJson()
* @see #encodedXml()
*/
T accept(String theHeaderValue);
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IOperationUntypedWithInput<T> extends IClientExecutable<IOperationUntypedWithInput<T>, T> {
@ -43,4 +44,9 @@ public interface IOperationUntypedWithInput<T> extends IClientExecutable<IOperat
*/
<R extends IBaseResource> IOperationUntypedWithInput<R> returnResourceType(Class<R> theReturnType);
/**
* Request that the method chain returns a {@link MethodOutcome} object. This object
* will contain details
*/
IOperationUntypedWithInput<MethodOutcome> returnMethodOutcome();
}

View File

@ -100,7 +100,11 @@ public class TokenParam extends BaseParam /*implements IQueryParameterType*/ {
@Override
String doGetValueAsQueryToken(FhirContext theContext) {
if (getSystem() != null) {
if (getValue() != null) {
return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|' + ParameterUtil.escape(getValue());
} else {
return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|';
}
}
return ParameterUtil.escape(getValue());
}

View File

@ -25,6 +25,7 @@ public enum VersionEnum {
V3_3_0,
V3_4_0,
V3_5_0,
V3_6_0
V3_6_0,
V3_7_0
}

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -188,7 +188,7 @@
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<!-- Dependencies for Schematron -->

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -43,6 +43,14 @@
<classifier>classes</classifier>
</dependency>
<!--
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojbc8g</artifactId>
<version>12.2.0.1</version>
</dependency>
-->
</dependencies>
<build>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -94,4 +94,9 @@ public class OkHttpRestfulRequest implements IHttpRequest {
return myRequestTypeEnum.name();
}
@Override
public void removeHeaders(String theHeaderName) {
myRequestBuilder.removeHeader(theHeaderName);
}
}

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -20,12 +20,11 @@ package ca.uhn.fhir.rest.client.apache;
* #L%
*/
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
@ -34,8 +33,9 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
/**
* A Http Request based on Apache. This is an adapter around the class
@ -79,6 +79,7 @@ public class ApacheHttpRequest implements IHttpRequest {
/**
* Get the ApacheRequest
*
* @return the ApacheRequest
*/
public HttpRequestBase getApacheRequest() {
@ -90,6 +91,12 @@ public class ApacheHttpRequest implements IHttpRequest {
return myRequest.getMethod();
}
@Override
public void removeHeaders(String theHeaderName) {
Validate.notBlank(theHeaderName, "theHeaderName must not be null or blank");
myRequest.removeHeaders(theHeaderName);
}
@Override
public String getRequestBodyFromStream() throws IOException {
if (myRequest instanceof HttpEntityEnclosingRequest) {

View File

@ -33,19 +33,20 @@ import ca.uhn.fhir.rest.client.method.IClientResponseHandler;
import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary;
import ca.uhn.fhir.rest.client.method.MethodUtil;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.XmlDetectionUtil;
import com.google.common.base.Charsets;
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 java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.*;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseClient implements IRestfulClient {
@ -93,9 +94,10 @@ public abstract class BaseClient implements IRestfulClient {
}
protected Map<String, List<String>> createExtraParams() {
HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>();
protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) {
HashMap<String, List<String>> retVal = new LinkedHashMap<>();
if (isBlank(theCustomAcceptHeader)) {
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
if (getEncoding() == EncodingEnum.XML) {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
@ -103,6 +105,7 @@ public abstract class BaseClient implements IRestfulClient {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
}
}
}
if (isPrettyPrint()) {
retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE));
@ -115,7 +118,7 @@ public abstract class BaseClient implements IRestfulClient {
public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl);
ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null);
}
void forceConformanceCheck() {
@ -200,11 +203,11 @@ public abstract class BaseClient implements IRestfulClient {
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null);
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null, null);
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) {
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader) {
if (!myDontValidateConformance) {
myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
@ -215,10 +218,10 @@ public abstract class BaseClient implements IRestfulClient {
IHttpRequest httpRequest = null;
IHttpResponse response = null;
try {
Map<String, List<String>> params = createExtraParams();
Map<String, List<String>> params = createExtraParams(theCustomAcceptHeader);
if (clientInvocation instanceof HttpGetClientInvocation) {
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) {
if (theEncoding == EncodingEnum.XML) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (theEncoding == EncodingEnum.JSON) {
@ -248,6 +251,11 @@ public abstract class BaseClient implements IRestfulClient {
httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint);
if (isNotBlank(theCustomAcceptHeader)) {
httpRequest.removeHeaders(Constants.HEADER_ACCEPT);
httpRequest.addHeader(Constants.HEADER_ACCEPT, theCustomAcceptHeader);
}
if (theCacheControlDirective != null) {
StringBuilder b = new StringBuilder();
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache());
@ -289,14 +297,10 @@ public abstract class BaseClient implements IRestfulClient {
if (response.getStatus() < 200 || response.getStatus() > 299) {
String body = null;
Reader reader = null;
try {
reader = response.createReader();
try (Reader reader = response.createReader()) {
body = IOUtils.toString(reader);
} catch (Exception e) {
ourLog.debug("Failed to read input stream", e);
} finally {
IOUtils.closeQuietly(reader);
}
String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo();
@ -334,27 +338,24 @@ public abstract class BaseClient implements IRestfulClient {
if (binding instanceof IClientResponseHandlerHandlesBinary) {
IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding;
if (handlesBinary.isBinary()) {
InputStream reader = response.readEntity();
try {
return handlesBinary.invokeClient(mimeType, reader, response.getStatus(), headers);
} finally {
IOUtils.closeQuietly(reader);
try (InputStream reader = response.readEntity()) {
return handlesBinary.invokeClientForBinary(mimeType, reader, response.getStatus(), headers);
}
}
}
Reader reader = response.createReader();
try (InputStream inputStream = response.readEntity()) {
InputStream inputStreamToReturn = inputStream;
if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
String responseString = IOUtils.toString(reader);
if (inputStream != null) {
String responseString = IOUtils.toString(inputStream, Charsets.UTF_8);
keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
reader = new StringReader(responseString);
inputStreamToReturn = new ByteArrayInputStream(responseString.getBytes(Charsets.UTF_8));
}
}
try {
return binding.invokeClient(mimeType, reader, response.getStatus(), headers);
} finally {
IOUtils.closeQuietly(reader);
return binding.invokeClient(mimeType, inputStreamToReturn, response.getStatus(), headers);
}
} catch (DataFormatException e) {
@ -463,7 +464,48 @@ public abstract class BaseClient implements IRestfulClient {
myInterceptors.remove(theInterceptor);
}
protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> {
@Override
public IBaseResource invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
/*
* For operation responses, if the response content type is a FHIR content-type
* (which is will probably almost always be) we just handle it normally. However,
* if we get back a successful (2xx) response from an operation, and the content
* type is something other than FHIR, we'll return it as a Binary wrapped in
* a Parameters resource.
*/
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType != null || theResponseStatusCode < 200 || theResponseStatusCode >= 300) {
return super.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
}
// Create a Binary resource to return
IBaseBinary responseBinary = BinaryUtil.newBinary(getFhirContext());
// Fetch the content type
String contentType = null;
List<String> contentTypeHeaders = theHeaders.get(Constants.HEADER_CONTENT_TYPE_LC);
if (contentTypeHeaders != null && contentTypeHeaders.size() > 0) {
contentType = contentTypeHeaders.get(0);
}
responseBinary.setContentType(contentType);
// Fetch the content itself
try {
responseBinary.setContent(IOUtils.toByteArray(theResponseInputStream));
} catch (IOException e) {
throw new InternalErrorException("IO failure parsing response", e);
}
return responseBinary;
}
}
protected class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
private boolean myAllowHtmlResponse;
private IIdType myId;
@ -498,20 +540,20 @@ public abstract class BaseClient implements IRestfulClient {
}
@Override
public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) {
return readHtmlResponse(theResponseReader);
return readHtmlResponse(theResponseInputStream);
}
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
IParser parser = respType.newParser(getFhirContext());
parser.setServerBaseUrl(getUrlBase());
if (myPreferResponseTypes != null) {
parser.setPreferTypes(myPreferResponseTypes);
}
T retVal = parser.parseResource(myReturnType, theResponseReader);
T retVal = parser.parseResource(myReturnType, theResponseInputStream);
MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal);
@ -519,7 +561,7 @@ public abstract class BaseClient implements IRestfulClient {
}
@SuppressWarnings("unchecked")
private T readHtmlResponse(Reader theResponseReader) {
private T readHtmlResponse(InputStream theResponseInputStream) {
RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType);
IBaseResource instance = resDef.newInstance();
BaseRuntimeChildDefinition textChild = resDef.getChildByName("text");
@ -531,7 +573,7 @@ public abstract class BaseClient implements IRestfulClient {
BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div");
IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance();
try {
divInstance.setValueAsString(IOUtils.toString(theResponseReader));
divInstance.setValueAsString(IOUtils.toString(theResponseInputStream, Charsets.UTF_8));
} catch (Exception e) {
throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e);
}
@ -539,8 +581,9 @@ public abstract class BaseClient implements IRestfulClient {
return (T) instance;
}
public void setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) {
public ResourceResponseHandler<T> setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) {
myPreferResponseTypes = thePreferResponseTypes;
return this;
}
}

View File

@ -46,19 +46,19 @@ import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.util.ICallable;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
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 java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.*;
import java.util.Map.Entry;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.*;
/**
* @author James Agnew
@ -98,7 +98,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private <T extends IBaseResource> T doReadOrVRead(final Class<T> theType, IIdType theId, boolean theVRead, ICallable<T> theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint,
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements, String theCustomAcceptHeaderValue) {
String resName = toResourceName(theType);
IIdType id = theId;
if (!id.hasBaseUrl()) {
@ -120,7 +120,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(theCustomAcceptHeaderValue), getEncoding(), isPrettyPrint());
}
if (theIfVersionMatches != null) {
@ -131,10 +131,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
ResourceResponseHandler<T> binding = new ResourceResponseHandler<>(theType, (Class<? extends IBaseResource>) null, id, allowHtmlResponse);
if (theNotModifiedHandler == null) {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue);
}
try {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue);
} catch (NotModifiedException e) {
return theNotModifiedHandler.call();
}
@ -228,7 +228,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override
public <T extends IBaseResource> T read(final Class<T> theType, UriDt theUrl) {
IdDt id = theUrl instanceof IdDt ? ((IdDt) theUrl) : new IdDt(theUrl);
return doReadOrVRead(theType, id, false, null, null, false, null, null, null);
return doReadOrVRead(theType, id, false, null, null, false, null, null, null, null);
}
@Override
@ -269,7 +269,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
public MethodOutcome update(IdDt theIdDt, IBaseResource theResource) {
BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(theResource, null, theIdDt, myContext);
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint());
}
OutcomeResponseHandler binding = new OutcomeResponseHandler();
@ -293,7 +293,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
invocation = ValidateMethodBindingDstu2Plus.createValidateInvocation(myContext, theResource);
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint());
}
OutcomeResponseHandler binding = new OutcomeResponseHandler();
@ -306,7 +306,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (theId.hasVersionIdPart() == false) {
throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_NO_VERSION_ID_FOR_VREAD, theId.getValue()));
}
return doReadOrVRead(theType, theId, true, null, null, false, null, null, null);
return doReadOrVRead(theType, theId, true, null, null, false, null, null, null, null);
}
@Override
@ -315,49 +315,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
return vread(theType, resId);
}
private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<>());
}
params.get(parameterName).add(parameterValue);
}
private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) {
if (thePrefer != null) {
theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue());
}
}
private static String validateAndEscapeConditionalUrl(String theSearchUrl) {
Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null");
StringBuilder b = new StringBuilder();
boolean haveHadQuestionMark = false;
for (int i = 0; i < theSearchUrl.length(); i++) {
char nextChar = theSearchUrl.charAt(i);
if (!haveHadQuestionMark) {
if (nextChar == '?') {
haveHadQuestionMark = true;
} else if (!Character.isLetter(nextChar)) {
throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl);
}
b.append(nextChar);
} else {
switch (nextChar) {
case '|':
case '?':
case '$':
case ':':
b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar)));
break;
default:
b.append(nextChar);
break;
}
}
}
return b.toString();
}
private enum MetaOperation {
ADD,
DELETE,
@ -366,14 +323,25 @@ public class GenericClient extends BaseClient implements IGenericClient {
private abstract class BaseClientExecutable<T extends IClientExecutable<?, Y>, Y> implements IClientExecutable<T, Y> {
protected EncodingEnum myParamEncoding;
protected Boolean myPrettyPrint;
protected SummaryEnum mySummaryMode;
protected CacheControlDirective myCacheControlDirective;
EncodingEnum myParamEncoding;
Boolean myPrettyPrint;
SummaryEnum mySummaryMode;
CacheControlDirective myCacheControlDirective;
private String myCustomAcceptHeaderValue;
private List<Class<? extends IBaseResource>> myPreferResponseTypes;
private boolean myQueryLogRequestAndResponse;
private HashSet<String> mySubsetElements;
public String getCustomAcceptHeaderValue() {
return myCustomAcceptHeaderValue;
}
@Override
public T accept(String theHeaderValue) {
myCustomAcceptHeaderValue = theHeaderValue;
return (T) this;
}
@Deprecated // override deprecated method
@SuppressWarnings("unchecked")
@Override
@ -392,7 +360,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override
public T elementsSubset(String... theElements) {
if (theElements != null && theElements.length > 0) {
mySubsetElements = new HashSet<String>(Arrays.asList(theElements));
mySubsetElements = new HashSet<>(Arrays.asList(theElements));
} else {
mySubsetElements = null;
}
@ -444,7 +412,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
myLastRequest = theInvocation.asHttpRequest(getServerBase(), theParams, getEncoding(), myPrettyPrint);
}
Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective);
Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective, myCustomAcceptHeaderValue);
return resp;
}
@ -461,7 +429,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
public T preferResponseType(Class<? extends IBaseResource> theClass) {
myPreferResponseTypes = null;
if (theClass != null) {
myPreferResponseTypes = new ArrayList<Class<? extends IBaseResource>>();
myPreferResponseTypes = new ArrayList<>();
myPreferResponseTypes.add(theClass);
}
return (T) this;
@ -1021,14 +989,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
IParser parser = respType.newParser(myContext);
RuntimeResourceDefinition type = myContext.getResourceDefinition("Parameters");
IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseReader);
IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseInputStream);
BaseRuntimeChildDefinition paramChild = type.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
@ -1061,6 +1029,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private Class myReturnResourceType;
private Class<? extends IBaseResource> myType;
private boolean myUseHttpGet;
private boolean myReturnMethodOutcome;
@SuppressWarnings("unchecked")
private void addParam(String theName, IBase theValue) {
@ -1170,11 +1139,19 @@ public class GenericClient extends BaseClient implements IGenericClient {
Object retVal = invoke(null, handler, invocation);
return retVal;
}
ResourceResponseHandler handler;
handler = new ResourceResponseHandler();
handler.setPreferResponseTypes(getPreferResponseTypes(myType));
IClientResponseHandler handler = new ResourceOrBinaryResponseHandler()
.setPreferResponseTypes(getPreferResponseTypes(myType));
if (myReturnMethodOutcome) {
handler = new MethodOutcomeResponseHandler(handler);
}
Object retVal = invoke(null, handler, invocation);
if (myReturnMethodOutcome) {
return retVal;
}
if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) {
return retVal;
}
@ -1236,6 +1213,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IOperationUntypedWithInput returnMethodOutcome() {
myReturnMethodOutcome = true;
return this;
}
@SuppressWarnings("unchecked")
@Override
public IOperationProcessMsgMode setMessageBundle(IBaseBundle theMsgBundle) {
@ -1331,10 +1314,30 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private final class MethodOutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private final IClientResponseHandler<? extends IBaseResource> myWrap;
private MethodOutcomeResponseHandler(IClientResponseHandler<? extends IBaseResource> theWrap) {
myWrap = theWrap;
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
IBaseResource response = myWrap.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
MethodOutcome retVal = new MethodOutcome();
retVal.setResource(response);
retVal.setCreatedUsingStatusCode(theResponseStatusCode);
retVal.setResponseHeaders(theHeaders);
return retVal;
}
}
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
@ -1344,7 +1347,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
IBaseOperationOutcome retVal;
try {
// TODO: handle if something else than OO comes back
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseReader);
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream);
} catch (DataFormatException e) {
ourLog.warn("Failed to parse OperationOutcome response", e);
return null;
@ -1368,11 +1371,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
response.setCreated(true);
}
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
response.setCreatedUsingStatusCode(theResponseStatusCode);
if (myPrefer == PreferReturnEnum.REPRESENTATION) {
if (response.getResource() == null) {
@ -1384,6 +1385,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
response.setResponseHeaders(theHeaders);
return response;
}
}
@ -1511,9 +1514,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override
public Object execute() {// AAA
if (myId.hasVersionIdPart()) {
return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements());
return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue());
}
return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements());
return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue());
}
@Override
@ -1636,11 +1639,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public List<IBaseResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public List<IBaseResource> invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
Class<? extends IBaseResource> bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass();
ResourceResponseHandler<IBaseResource> handler = new ResourceResponseHandler<IBaseResource>((Class<IBaseResource>) bundleType);
IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders);
ResourceResponseHandler<IBaseResource> handler = new ResourceResponseHandler<>((Class<IBaseResource>) bundleType);
IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory();
bundleFactory.initializeWithBundleResource(response);
return bundleFactory.toListOfResources();
@ -1937,9 +1940,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
private final class StringResponseHandler implements IClientResponseHandler<String> {
@Override
public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public String invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws IOException, BaseServerResponseException {
return IOUtils.toString(theResponseReader);
return IOUtils.toString(theResponseInputStream, Charsets.UTF_8);
}
}
@ -2251,4 +2254,47 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<>());
}
params.get(parameterName).add(parameterValue);
}
private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) {
if (thePrefer != null) {
theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue());
}
}
private static String validateAndEscapeConditionalUrl(String theSearchUrl) {
Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null");
StringBuilder b = new StringBuilder();
boolean haveHadQuestionMark = false;
for (int i = 0; i < theSearchUrl.length(); i++) {
char nextChar = theSearchUrl.charAt(i);
if (!haveHadQuestionMark) {
if (nextChar == '?') {
haveHadQuestionMark = true;
} else if (!Character.isLetter(nextChar)) {
throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl);
}
b.append(nextChar);
} else {
switch (nextChar) {
case '|':
case '?':
case '$':
case ':':
b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar)));
break;
default:
b.append(nextChar);
break;
}
}
}
return b.toString();
}
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method;
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.*;
@ -71,11 +72,11 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
}
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {
EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType);
if (encoding == null) {
NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
populateException(ex, theResponseReader);
NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
populateException(ex, theResponseInputStream);
throw ex;
}
@ -139,7 +140,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return mySupportsConditionalMultiple;
}
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) {
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, InputStream theResponseInputStream) {
BaseServerResponseException ex;
switch (theStatusCode) {
case Constants.STATUS_HTTP_400_BAD_REQUEST:
@ -158,9 +159,9 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
ex = new PreconditionFailedException("Server responded with HTTP 412");
break;
case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theStatusCode, null);
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theStatusCode, null);
// TODO: handle if something other than OO comes back
BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseReader);
BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseInputStream);
ex = new UnprocessableEntityException(myContext, operationOutcome);
break;
default:
@ -168,7 +169,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
break;
}
populateException(ex, theResponseReader);
populateException(ex, theResponseInputStream);
return ex;
}
@ -322,9 +323,9 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class);
}
private static void populateException(BaseServerResponseException theEx, Reader theResponseReader) {
private static void populateException(BaseServerResponseException theEx, InputStream theResponseInputStream) {
try {
String responseText = IOUtils.toString(theResponseReader);
String responseText = IOUtils.toString(theResponseInputStream);
theEx.setResponseBody(responseText);
} catch (IOException e) {
ourLog.debug("Failed to read response", e);

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.client.method;
* #L%
*/
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.*;
@ -68,15 +69,15 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
protected abstract String getMatchingOperation();
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
if (theResponseStatusCode >= 200 && theResponseStatusCode < 300) {
if (myReturnVoid) {
return null;
}
MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
return retVal;
}
throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
public boolean isReturnVoid() {

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.client.method;
* #L%
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@ -123,21 +125,21 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
public abstract ReturnTypeEnum getReturnType();
@Override
public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) {
public Object invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException {
if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) {
return toReturnType(null);
}
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theResponseStatusCode, myPreferTypesList);
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList);
switch (getReturnType()) {
case BUNDLE: {
IBaseBundle bundle = null;
List<? extends IBaseResource> listOfResources = null;
IBaseBundle bundle;
List<? extends IBaseResource> listOfResources;
Class<? extends IBaseResource> type = getContext().getResourceDefinition("Bundle").getImplementingClass();
bundle = (IBaseBundle) parser.parseResource(type, theResponseReader);
bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream);
listOfResources = BundleUtil.toListOfResources(getContext(), bundle);
switch (getMethodReturnType()) {
@ -171,9 +173,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
case RESOURCE: {
IBaseResource resource;
if (myResourceType != null) {
resource = parser.parseResource(myResourceType, theResponseReader);
resource = parser.parseResource(myResourceType, theResponseInputStream);
} else {
resource = parser.parseResource(theResponseReader);
resource = parser.parseResource(theResponseInputStream);
}
MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method;
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Map;
@ -29,6 +30,6 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
public interface IClientResponseHandler<T> {
T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
}

View File

@ -35,6 +35,6 @@ public interface IClientResponseHandlerHandlesBinary<T> extends IClientResponseH
*/
boolean isBinary();
T invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
T invokeClientForBinary(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
}

View File

@ -49,15 +49,6 @@ import ca.uhn.fhir.util.*;
public class MethodUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class);
private static final Set<String> ourServletRequestTypes = new HashSet<String>();
private static final Set<String> ourServletResponseTypes = new HashSet<String>();
static {
ourServletRequestTypes.add("javax.servlet.ServletRequest");
ourServletResponseTypes.add("javax.servlet.ServletResponse");
ourServletRequestTypes.add("javax.servlet.http.HttpServletRequest");
ourServletResponseTypes.add("javax.servlet.http.HttpServletResponse");
}
/** Non instantiable */
private MethodUtil() {
@ -497,8 +488,8 @@ public class MethodUtil {
}
public static MethodOutcome process2xxResponse(FhirContext theContext, int theResponseStatusCode,
String theResponseMimeType, Reader theResponseReader, Map<String, List<String>> theHeaders) {
List<String> locationHeaders = new ArrayList<String>();
String theResponseMimeType, InputStream theResponseReader, Map<String, List<String>> theHeaders) {
List<String> locationHeaders = new ArrayList<>();
List<String> lh = theHeaders.get(Constants.HEADER_LOCATION_LC);
if (lh != null) {
locationHeaders.addAll(lh);
@ -509,14 +500,14 @@ public class MethodUtil {
}
MethodOutcome retVal = new MethodOutcome();
if (locationHeaders != null && locationHeaders.size() > 0) {
if (locationHeaders.size() > 0) {
String locationHeader = locationHeaders.get(0);
BaseOutcomeReturningMethodBinding.parseContentLocation(theContext, retVal, locationHeader);
}
if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) {
EncodingEnum ct = EncodingEnum.forContentType(theResponseMimeType);
if (ct != null) {
PushbackReader reader = new PushbackReader(theResponseReader);
PushbackInputStream reader = new PushbackInputStream(theResponseReader);
try {
int firstByte = reader.read();

View File

@ -106,7 +106,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem
}
@Override
public Object invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public Object invokeClientForBinary(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws IOException, BaseServerResponseException {
byte[] contents = IOUtils.toByteArray(theResponseReader);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -16,14 +16,14 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
</dependency>
<!-- Server -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
@ -35,43 +35,43 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2.1</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu3</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -85,6 +85,11 @@ public class JaxRsHttpRequest implements IHttpRequest {
return myRequestType.name();
}
@Override
public void removeHeaders(String theHeaderName) {
myHeaders.remove(theHeaderName);
}
/**
* Get the Request
*

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.jaxrs.client;
import java.io.IOException;
import java.io.*;
/*
* #%L
@ -22,9 +22,6 @@ import java.io.IOException;
* #L%
*/
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -34,9 +31,11 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import ca.uhn.fhir.rest.client.impl.BaseHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import org.apache.commons.io.IOUtils;
/**
* A Http Response based on JaxRs. This is an adapter around the class {@link javax.ws.rs.core.Response Response}
@ -118,7 +117,11 @@ public class JaxRsHttpResponse extends BaseHttpResponse implements IHttpResponse
@Override
public InputStream readEntity() {
return myResponse.readEntity(java.io.InputStream.class);
if (!myBufferedEntity && !myResponse.hasEntity()) {
return new ByteArrayInputStream(new byte[0]);
} else {
return new ByteArrayInputStream(myResponse.readEntity(byte[].class));
}
}
@Override

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.6.0</version>
<version>3.7.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -163,7 +163,7 @@
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<!-- For UCUM: TODO we should replace this with org.fhir UCUM -->
@ -670,7 +670,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} -Dfile.encoding=UTF-8 -Xmx1024m</argLine>
<argLine>@{argLine} -Dfile.encoding=UTF-8 -Xmx20484M -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=2048M -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC</argLine>
<forkCount>0.6C</forkCount>
</configuration>
</plugin>

View File

@ -1,5 +1,35 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import javax.annotation.Nonnull;
/*
* #%L
* HAPI FHIR JPA Server
@ -20,46 +50,14 @@ package ca.uhn.fhir.jpa.config;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.*;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.hibernate5.HibernateExceptionTranslator;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import javax.annotation.Nonnull;
import java.util.concurrent.ScheduledExecutorService;
@Configuration
@EnableScheduling
@EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data")
@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters={
@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=BaseConfig.class),
@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=WebSocketConfigurer.class)})
public abstract class BaseConfig implements SchedulingConfigurer {
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
@ -67,11 +65,6 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Autowired
protected Environment myEnv;
@Bean(name = "myDaoRegistry")
public DaoRegistry daoRegistry() {
return new DaoRegistry();
}
@Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) {
theTaskRegistrar.setTaskScheduler(taskScheduler());
@ -95,27 +88,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
public abstract FhirContext fhirContext();
@Bean
public ICacheWarmingSvc cacheWarmingSvc() {
return new CacheWarmingSvcImpl();
}
@Bean
public HibernateExceptionTranslator hibernateExceptionTranslator() {
return new HibernateExceptionTranslator();
}
@Bean
public HibernateJpaDialect hibernateJpaDialectInstance() {
return new HibernateJpaDialect();
}
@Bean()
public ScheduledExecutorService scheduledExecutorService() {
public ScheduledExecutorFactoryBean scheduledExecutorService() {
ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean();
b.setPoolSize(5);
b.afterPropertiesSet();
return b.getObject();
return b;
}
@Bean(name = "mySubscriptionTriggeringProvider")
@ -124,17 +102,28 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new SubscriptionTriggeringProvider();
}
@Bean(autowire = Autowire.BY_TYPE, name = "mySearchCoordinatorSvc")
public ISearchCoordinatorSvc searchCoordinatorSvc() {
return new SearchCoordinatorSvcImpl();
@Bean()
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService().getObject());
retVal.setScheduledExecutor(scheduledExecutorService().getObject());
return retVal;
}
@Bean(name = TASK_EXECUTOR_NAME)
public AsyncTaskExecutor taskExecutor() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService().getObject());
retVal.setScheduledExecutor(scheduledExecutorService().getObject());
return retVal;
}
@Bean
public ISearchParamPresenceSvc searchParamPresenceSvc() {
return new SearchParamPresenceSvcImpl();
public IResourceReindexingSvc resourceReindexingSvc() {
return new ResourceReindexingSvcImpl();
}
@Bean(autowire = Autowire.BY_TYPE)
@Bean
public IStaleSearchDeletingSvc staleSearchDeletingSvc() {
return new StaleSearchDeletingSvcImpl();
}
@ -162,19 +151,6 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new SubscriptionWebsocketInterceptor();
}
@Bean(name = TASK_EXECUTOR_NAME)
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService());
retVal.setScheduledExecutor(scheduledExecutorService());
return retVal;
}
@Bean
public IResourceReindexingSvc resourceReindexingSvc() {
return new ResourceReindexingSvcImpl();
}
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.entity");
@ -184,13 +160,4 @@ public abstract class BaseConfig implements SchedulingConfigurer {
private static HibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) {
return new HapiFhirHibernateJpaDialect(theLocalizer);
}
/**
* This lets the "@Value" fields reference properties from the properties file
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}

View File

@ -1,126 +1,11 @@
package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import java.io.CharArrayWriter;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.*;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.BaseResource;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.index.IndexingSupport;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.BaseTag;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTag;
import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.SearchStatusEnum;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
import ca.uhn.fhir.jpa.dao.index.SearchParamExtractorService;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
@ -129,8 +14,6 @@ import ca.uhn.fhir.jpa.util.DeleteConflict;
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.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
@ -145,25 +28,11 @@ import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.LenientErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
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.param.ParameterUtil;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.UriAndListParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
@ -172,6 +41,45 @@ import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.XmlUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.Validate;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.*;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import java.io.CharArrayWriter;
import java.text.Normalizer;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.commons.lang3.StringUtils.*;
/*
* #%L
@ -195,7 +103,7 @@ import ca.uhn.fhir.util.XmlUtil;
@SuppressWarnings("WeakerAccess")
@Repository
public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware, IndexingSupport {
public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware {
public static final long INDEX_STATUS_INDEXED = 1L;
public static final long INDEX_STATUS_INDEXING_FAILED = 2L;
@ -204,46 +112,17 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
public static final String OO_SEVERITY_INFO = "information";
public static final String OO_SEVERITY_WARN = "warning";
public static final String UCUM_NS = "http://unitsofmeasure.org";
static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
static final Map<String, Class<? extends IQueryParameterAnd<?>>> RESOURCE_META_AND_PARAMS;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>();
private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
private static boolean ourValidationDisabledForUnitTest;
private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
Map<String, Class<? extends IQueryParameterAnd<?>>> resourceMetaAndParams = new HashMap<String, Class<? extends IQueryParameterAnd<?>>>();
resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class);
resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class);
resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class);
resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class);
resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class);
resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<String>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
}
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
protected IdHelperService myIdHelperService;
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
protected ISearchResultDao mySearchResultDao;
@ -255,6 +134,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao;
@Autowired()
protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao;
@Autowired
protected IResourceLinkDao myResourceLinkDao;
@Autowired()
protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao;
@Autowired()
@ -279,6 +160,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
protected IResourceTagDao myResourceTagDao;
@Autowired
protected IResourceSearchViewDao myResourceViewDao;
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@Autowired(required = true)
private DaoConfig myConfig;
private FhirContext myContext;
@ -290,20 +173,20 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
private ISearchParamExtractor mySearchParamExtractor;
@Autowired
private ISearchParamPresenceSvc mySearchParamPresenceSvc;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
//@Autowired
//private ISearchResultDao mySearchResultDao;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private ApplicationContext myApplicationContext;
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
@Autowired
private BeanFactory beanFactory;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private SearchParamExtractorService mySearchParamExtractorService;
public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
}
}
private ApplicationContext myApplicationContext;
/**
* Returns the newly created forced ID. If the entity already had a forced ID, or if
@ -311,7 +194,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/
protected ForcedId createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId, boolean theCreateForPureNumericIds) {
if (theId.isEmpty() == false && theId.hasIdPart() && theEntity.getForcedId() == null) {
if (!theCreateForPureNumericIds && isValidPid(theId)) {
if (!theCreateForPureNumericIds && IdHelperService.isValidPid(theId)) {
return null;
}
@ -328,6 +211,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
protected ExpungeOutcome doExpunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions) {
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions);
if (!getConfig().isExpungeEnabled()) {
@ -367,11 +251,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
});
/*
* Delete any search result cache entries pointing to the given resource
* Delete any search result cache entries pointing to the given resource. We do
* this in batches to avoid sending giant batches of parameters to the DB
*/
if (resourceIds.getContent().size() > 0) {
List<List<Long>> partitions = Lists.partition(resourceIds.getContent(), 800);
for (List<Long> nextPartition : partitions) {
ourLog.info("Expunging any search results pointing to {} resources", nextPartition.size());
txTemplate.execute(t -> {
mySearchResultDao.deleteByResourceIds(resourceIds.getContent());
mySearchResultDao.deleteByResourceIds(nextPartition);
return null;
});
}
@ -438,7 +325,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
ourLog.info("** BEGINNING GLOBAL $expunge **");
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(t -> {
doExpungeEverythingQuery("UPDATE " + ResourceHistoryTable.class.getSimpleName() + " d SET d.myForcedId = null");
doExpungeEverythingQuery("UPDATE " + ResourceTable.class.getSimpleName() + " d SET d.myForcedId = null");
@ -521,6 +408,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myResourceIndexedSearchParamQuantityDao.deleteAll(resource.getParamsQuantity());
myResourceIndexedSearchParamStringDao.deleteAll(resource.getParamsString());
myResourceIndexedSearchParamTokenDao.deleteAll(resource.getParamsToken());
myResourceLinkDao.deleteAll(resource.getResourceLinks());
myResourceLinkDao.deleteAll(resource.getResourceLinksAsTarget());
myResourceTagDao.deleteAll(resource.getTags());
resource.getTags().clear();
@ -529,7 +418,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
ForcedId forcedId = resource.getForcedId();
resource.setForcedId(null);
myResourceTableDao.saveAndFlush(resource);
myForcedIdDao.delete(forcedId);
myIdHelperService.delete(forcedId);
}
myResourceTableDao.delete(resource);
@ -560,7 +449,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set<ResourceTag> allDefs) {
TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
if (tagList != null) {
@ -647,7 +535,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theResourceName != null) {
Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName);
if (theResourceId != null) {
cq.where(typePredicate, builder.equal(from.get("myResourceId"), translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
cq.where(typePredicate, builder.equal(from.get("myResourceId"), myIdHelperService.translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
} else {
cq.where(typePredicate);
}
@ -659,6 +547,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
}
protected void flushJpaSession() {
SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
int insertionCount = session.getActionQueue().numberOfInsertions();
@ -679,8 +568,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
@Override
public DaoConfig getConfig() {
protected DaoConfig getConfig() {
return myConfig;
}
@ -710,51 +598,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
@Override
@SuppressWarnings("unchecked")
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> resourceTypeToDao = getDaos();
IFhirResourceDao<R> dao = (IFhirResourceDao<R>) resourceTypeToDao.get(theType);
return dao;
return myDaoRegistry.getResourceDaoIfExists(theType);
}
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getDaos() {
if (myResourceTypeToDao == null) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> resourceTypeToDao = new HashMap<>();
Map<String, IFhirResourceDao> daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false);
String[] beanNames = myApplicationContext.getBeanNamesForType(IFhirResourceDao.class);
for (IFhirResourceDao<?> next : daos.values()) {
resourceTypeToDao.put(next.getResourceType(), next);
}
if (this instanceof IFhirResourceDao<?>) {
IFhirResourceDao<?> thiz = (IFhirResourceDao<?>) this;
resourceTypeToDao.put(thiz.getResourceType(), thiz);
}
myResourceTypeToDao = resourceTypeToDao;
}
return Collections.unmodifiableMap(myResourceTypeToDao);
}
@Override
public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() {
return myResourceIndexedCompositeStringUniqueDao;
}
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
Map<String, RuntimeSearchParam> params = mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName());
return params.get(theParamName);
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()).values();
protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
return myDaoRegistry.getDaoOrThrowException(theClass);
}
protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -892,39 +742,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
}
@Override
public boolean isLogicalReference(IIdType theId) {
Set<String> treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical();
if (treatReferencesAsLogical != null) {
for (String nextLogicalRef : treatReferencesAsLogical) {
nextLogicalRef = trim(nextLogicalRef);
if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') {
if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) {
return true;
}
} else {
if (theId.getValue().equals(nextLogicalRef)) {
return true;
}
}
}
}
return false;
}
public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
}
return LogicalReferenceHelper.isLogicalReference(myConfig, theId);
}
@Override
public SearchBuilder newSearchBuilder() {
SearchBuilder builder = new SearchBuilder(
getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao,
myForcedIdDao, myTerminologySvc, mySerarchParamRegistry, myResourceTagDao, myResourceViewDao);
return builder;
return beanFactory.getBean(SearchBuilder.class, this);
}
public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
@ -944,34 +768,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
public String parseContentTextIntoWords(IBaseResource theResource) {
StringBuilder retVal = new StringBuilder();
@SuppressWarnings("rawtypes")
List<IPrimitiveType> childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class);
for (@SuppressWarnings("rawtypes")
IPrimitiveType nextType : childElements) {
if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) {
String nextValue = nextType.getValueAsString();
if (isNotBlank(nextValue)) {
retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
retVal.append("\n");
}
}
}
return retVal.toString();
}
@Override
public void populateFullTextFields(final IBaseResource theResource, ResourceTable theEntity) {
if (theEntity.getDeleted() != null) {
theEntity.setNarrativeTextParsedIntoWords(null);
theEntity.setContentTextParsedIntoWords(null);
} else {
theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource));
theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theResource));
}
}
private void populateResourceIdFromEntity(IBaseResourceEntity theEntity, final IBaseResource theResource) {
IIdType id = theEntity.getIdDt();
if (getContext().getVersion().getVersion().isRi()) {
@ -1006,7 +802,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theEntity.getDeleted() == null) {
encoding = myConfig.getResourceEncoding();
Set<String> excludeElements = EXCLUDE_ELEMENTS_IN_ENCODED;
Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
theEntity.setFhirVersion(myContext.getVersion().getVersion());
bytes = encodeResource(theResource, encoding, excludeElements, myContext);
@ -1247,25 +1043,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
// nothing
}
@Override
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType);
SearchParameterMap paramMap = translateMatchUrl(this, myContext, theMatchUrl, resourceDef);
paramMap.setLoadSynchronous(true);
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
}
IFhirResourceDao<R> dao = getDao(theResourceType);
if (dao == null) {
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
}
return dao.searchForIds(paramMap);
}
@CoverageIgnore
public BaseHasResource readEntity(IIdType theValueId) {
throw new NotImplementedException("");
@ -1283,7 +1060,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
/**
* This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds.
* <p>
@ -1334,11 +1110,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return false;
}
@PostConstruct
public void startClearCaches() {
myResourceTypeToDao = null;
}
private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) {
return new ExpungeOutcome()
.setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get());
@ -1452,7 +1223,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
@Override
public String toResourceName(Class<? extends IBaseResource> theResourceType) {
return myContext.getResourceDefinition(theResourceType).getName();
}
@ -1466,16 +1236,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return new SliceImpl<>(Collections.singletonList(theVersion.getId()));
}
@Override
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
return translateForcedIdToPids(getConfig(), new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
}
protected List<Long> translateForcedIdToPids(IIdType theId) {
return translateForcedIdToPids(getConfig(), theId, myForcedIdDao);
}
@SuppressWarnings("unchecked")
protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable
theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
@ -1507,14 +1267,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theEntity.setPublished(theUpdateTime);
}
ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(this, theEntity);
ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(theEntity);
ResourceIndexedSearchParams newParams = null;
EncodedResource changed;
if (theDeletedTimestampOrNull != null) {
newParams = new ResourceIndexedSearchParams(this);
newParams = new ResourceIndexedSearchParams();
theEntity.setDeleted(theDeletedTimestampOrNull);
theEntity.setUpdated(theDeletedTimestampOrNull);
@ -1530,7 +1290,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (thePerformIndexing) {
newParams = new ResourceIndexedSearchParams(this, theUpdateTime, theEntity, theResource, existingParams);
newParams = new ResourceIndexedSearchParams();
mySearchParamExtractorService.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
@ -1541,9 +1302,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
}
newParams.setParams(theEntity);
newParams.setParamsOn(theEntity);
theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
populateFullTextFields(theResource, theEntity);
populateFullTextFields(myContext, theResource, theEntity);
} else {
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
@ -1616,7 +1377,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/
if (thePerformIndexing) {
Map<String, Boolean> presentSearchParams = new HashMap<>();
// TODO KHS null check?
for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
presentSearchParams.put(nextKey, Boolean.TRUE);
}
@ -1635,8 +1395,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
* Indexing
*/
if (thePerformIndexing) {
newParams.removeCommon(theEntity, existingParams);
mySearchParamExtractorService.removeCommon(newParams, theEntity, existingParams);
} // if thePerformIndexing
if (theResource != null) {
@ -1836,6 +1595,50 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
@Override
public ISearchParamRegistry getSearchParamRegistry() {
return mySearchParamRegistry;
}
public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
}
}
public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
}
}
public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) {
StringBuilder retVal = new StringBuilder();
@SuppressWarnings("rawtypes")
List<IPrimitiveType> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class);
for (@SuppressWarnings("rawtypes")
IPrimitiveType nextType : childElements) {
if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) {
String nextValue = nextType.getValueAsString();
if (isNotBlank(nextValue)) {
retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
retVal.append("\n");
}
}
}
return retVal.toString();
}
public static void populateFullTextFields(final FhirContext theContext, final IBaseResource theResource, ResourceTable theEntity) {
if (theEntity.getDeleted() != null) {
theEntity.setNarrativeTextParsedIntoWords(null);
theEntity.setContentTextParsedIntoWords(null);
} else {
theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource));
theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theContext, theResource));
}
}
public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
String resourceText = null;
switch (theResourceEncoding) {
@ -1875,44 +1678,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return bytes;
}
protected static boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);
if (nextChar < '0' || nextChar > '9') {
return false;
}
}
return true;
}
@CoverageIgnore
protected static IQueryParameterAnd<?> newInstanceAnd(String chain) {
IQueryParameterAnd<?> type;
Class<? extends IQueryParameterAnd<?>> clazz = RESOURCE_META_AND_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
@CoverageIgnore
protected static IQueryParameterType newInstanceType(String chain) {
IQueryParameterType type;
Class<? extends IQueryParameterType> clazz = RESOURCE_META_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
public static String normalizeString(String theString) {
CharArrayWriter outBuffer = new CharArrayWriter(theString.length());
@ -1995,169 +1760,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
protected static Long translateForcedIdToPid(DaoConfig theDaoConfig, String theResourceName, String theResourceId, IForcedIdDao
theForcedIdDao) {
return translateForcedIdToPids(theDaoConfig, new IdDt(theResourceName, theResourceId), theForcedIdDao).get(0);
}
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
Validate.isTrue(theId.hasIdPart());
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
return Collections.singletonList(theId.getIdPartAsLong());
} else {
List<ForcedId> forcedId;
if (theId.hasResourceType()) {
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
} else {
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
}
if (forcedId.isEmpty() == false) {
List<Long> retVal = new ArrayList<>(forcedId.size());
for (ForcedId next : forcedId) {
retVal.add(next.getResourcePid());
}
return retVal;
} else {
throw new ResourceNotFoundException(theId);
}
}
}
public static SearchParameterMap translateMatchUrl(IDao theCallingDao, FhirContext theContext, String
theMatchUrl, RuntimeResourceDefinition resourceDef) {
SearchParameterMap paramMap = new SearchParameterMap();
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
for (NameValuePair next : parameters) {
if (isBlank(next.getValue())) {
continue;
}
String paramName = next.getName();
String qualifier = null;
for (int i = 0; i < paramName.length(); i++) {
switch (paramName.charAt(i)) {
case '.':
case ':':
qualifier = paramName.substring(i);
paramName = paramName.substring(0, i);
i = Integer.MAX_VALUE - 1;
break;
}
}
QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
nameToParamLists.put(paramName, paramList);
}
for (String nextParamName : nameToParamLists.keySet()) {
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
if (paramList != null && paramList.size() > 0) {
if (paramList.size() > 2) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions");
} else {
DateRangeParam p1 = new DateRangeParam();
p1.setValuesAsQueryTokens(theContext, nextParamName, paramList);
paramMap.setLastUpdated(p1);
}
}
continue;
}
if (Constants.PARAM_HAS.equals(nextParamName)) {
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
paramMap.add(nextParamName, param);
continue;
}
if (Constants.PARAM_COUNT.equals(nextParamName)) {
if (paramList.size() > 0 && paramList.get(0).size() > 0) {
String intString = paramList.get(0).get(0);
try {
paramMap.setCount(Integer.parseInt(intString));
} catch (NumberFormatException e) {
throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString);
}
}
continue;
}
if (RESOURCE_META_PARAMS.containsKey(nextParamName)) {
if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) {
throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier());
}
IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
type.setValuesAsQueryTokens(theContext, nextParamName, (paramList));
paramMap.add(nextParamName, type);
} else if (nextParamName.startsWith("_")) {
// ignore these since they aren't search params (e.g. _sort)
} else {
RuntimeSearchParam paramDef = theCallingDao.getSearchParamByName(resourceDef, nextParamName);
if (paramDef == null) {
throw new InvalidRequestException(
"Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName);
}
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, paramDef, nextParamName, paramList);
paramMap.add(nextParamName, param);
}
}
return paramMap;
}
public static List<NameValuePair> translateMatchUrl(String theMatchUrl) {
List<NameValuePair> parameters;
String matchUrl = theMatchUrl;
int questionMarkIndex = matchUrl.indexOf('?');
if (questionMarkIndex != -1) {
matchUrl = matchUrl.substring(questionMarkIndex + 1);
}
matchUrl = matchUrl.replace("|", "%7C");
matchUrl = matchUrl.replace("=>=", "=%3E%3D");
matchUrl = matchUrl.replace("=<=", "=%3C%3D");
matchUrl = matchUrl.replace("=>", "=%3E");
matchUrl = matchUrl.replace("=<", "=%3C");
if (matchUrl.contains(" ")) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)");
}
parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&');
return parameters;
}
public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
if (!theResourceName.equals(theEntity.getResourceType())) {
throw new ResourceNotFoundException(
"Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType());
}
}
@Override
public ISearchParamExtractor getSearchParamExtractor() {
return mySearchParamExtractor;
}
@Override
public ISearchParamRegistry getSearchParamRegistry() {
return mySearchParamRegistry;
}
@Override
public EntityManager getEntityManager() {
return myEntityManager;
}
@Override
public Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getResourceTypeToDao() {
return myResourceTypeToDao;
}
@Override
public IForcedIdDao getForcedIdDao() {
return myForcedIdDao;
}
}

View File

@ -25,7 +25,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
@ -75,8 +74,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
@Autowired()
protected ISearchResultDao mySearchResultDao;
@Autowired
protected DaoConfig myDaoConfig;
@Autowired
@ -86,6 +83,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private String mySecondaryPrimaryKeyParamName;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -272,7 +271,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequest) {
StopWatch w = new StopWatch();
Set<Long> resource = processMatchUrl(theUrl, myResourceType);
Set<Long> resource = myMatchUrlService.processMatchUrl(theUrl, myResourceType);
if (resource.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) {
throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
@ -372,7 +371,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity.setResourceType(toResourceName(theResource));
if (isNotBlank(theIfNoneExist)) {
Set<Long> match = processMatchUrl(theIfNoneExist, myResourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(theIfNoneExist, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
throw new PreconditionFailedException(msg);
@ -793,7 +792,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
myResourceName = def.getName();
if (mySecondaryPrimaryKeyParamName != null) {
RuntimeSearchParam sp = getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
RuntimeSearchParam sp = mySearchParamRegistry.getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
if (sp == null) {
throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]");
}
@ -849,7 +848,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public Set<Long> processMatchUrl(String theMatchUrl) {
return processMatchUrl(theMatchUrl, getResourceType());
return myMatchUrlService.processMatchUrl(theMatchUrl, getResourceType());
}
@Override
@ -911,7 +910,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
Long pid = translateForcedIdToPid(getResourceName(), theId.getIdPart());
Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
if (entity == null) {
@ -951,7 +950,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
protected ResourceTable readEntityLatestVersion(IIdType theId) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, translateForcedIdToPid(getResourceName(), theId.getIdPart()));
ResourceTable entity = myEntityManager.find(ResourceTable.class, myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart()));
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
@ -1192,7 +1191,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Should not be null since the check above would have caught it
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceName);
RuntimeSearchParam paramDef = getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
for (String nextValue : theSource.get(nextParamName)) {
if (isNotBlank(nextValue)) {
@ -1232,7 +1231,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
IIdType resourceId;
if (isNotBlank(theMatchUrl)) {
StopWatch sw = new StopWatch();
Set<Long> match = processMatchUrl(theMatchUrl, myResourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(theMatchUrl, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
throw new PreconditionFailedException(msg);

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
@ -51,8 +50,6 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
@Autowired
@Qualifier("myResourceCountsCache")
public ResourceCountCache myResourceCountsCache;
@Autowired
private IForcedIdDao myForcedIdDao;
private ReentrantLock myReindexLock = new ReentrantLock(false);
@Autowired
private ITermConceptDao myTermConceptDao;
@ -60,12 +57,9 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private IResourceTableDao myResourceTableDao;
@Override
@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.NEVER)
public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions) {
return doExpunge(null, null, null, theExpungeOptions);
}

View File

@ -348,5 +348,16 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
protected abstract RuntimeSearchParam toRuntimeSp(SP theNextSp);
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
Map<String, RuntimeSearchParam> params = getActiveSearchParams(theResourceDef.getName());
return params.get(theParamName);
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return getActiveSearchParams(theResourceDef.getName()).values();
}
}

View File

@ -156,6 +156,7 @@ public class DaoConfig {
private List<Integer> mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1);
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/**
@ -1448,6 +1449,50 @@ public class DaoConfig {
myDisableHashBasedSearches = theDisableHashBasedSearches;
}
/**
* If set to <code>false</code> (default is true) the server will not use
* in-memory subscription searching and instead use the database matcher for all subscription
* criteria matching.
* <p>
* When there are subscriptions registered
* on the server, the default behaviour is to compare the changed resource to the
* subscription criteria directly in-memory without going out to the database.
* Certain types of subscription criteria, e.g. chained references of queries with
* qualifiers or prefixes, are not supported by the in-memory matcher and will fall back
* to a database matcher.
* <p>
* The database matcher performs a query against the
* database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity
*
* @since 3.6.1
*/
public boolean isEnableInMemorySubscriptionMatching() {
return myEnableInMemorySubscriptionMatching;
}
/**
* If set to <code>false</code> (default is true) the server will not use
* in-memory subscription searching and instead use the database matcher for all subscription
* criteria matching.
* <p>
* When there are subscriptions registered
* on the server, the default behaviour is to compare the changed resource to the
* subscription criteria directly in-memory without going out to the database.
* Certain types of subscription criteria, e.g. chained references of queries with
* qualifiers or prefixes, are not supported by the in-memory matcher and will fall back
* to a database matcher.
* <p>
* The database matcher performs a query against the
* database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity
*
* @since 3.6.1
*/
public void setEnableInMemorySubscriptionMatching(boolean theEnableInMemorySubscriptionMatching) {
myEnableInMemorySubscriptionMatching = theEnableInMemorySubscriptionMatching;
}
public enum IndexEnabledEnum {
ENABLED,
DISABLED

View File

@ -23,22 +23,27 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component("myDaoRegistry")
public class DaoRegistry implements ApplicationContextAware {
private ApplicationContext myAppCtx;
@Autowired
private FhirContext myCtx;
private FhirContext myContext;
private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao;
private volatile IFhirSystemDao<?, ?> mySystemDao;
@ -47,8 +52,8 @@ public class DaoRegistry implements ApplicationContextAware {
myAppCtx = theApplicationContext;
}
public IFhirSystemDao<?, ?> getSystemDao() {
IFhirSystemDao<?, ?> retVal = mySystemDao;
public IFhirSystemDao getSystemDao() {
IFhirSystemDao retVal = mySystemDao;
if (retVal == null) {
retVal = myAppCtx.getBean(IFhirSystemDao.class);
mySystemDao = retVal;
@ -56,10 +61,11 @@ public class DaoRegistry implements ApplicationContextAware {
return retVal;
}
public IFhirResourceDao<?> getResourceDao(String theResourceName) {
IFhirResourceDao<?> retVal = getResourceNameToResourceDao().get(theResourceName);
public IFhirResourceDao getResourceDao(String theResourceName) {
init();
IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName);
if (retVal == null) {
List<String> supportedResourceTypes = getResourceNameToResourceDao()
List<String> supportedResourceTypes = myResourceNameToResourceDao
.keySet()
.stream()
.sorted()
@ -67,26 +73,54 @@ public class DaoRegistry implements ApplicationContextAware {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + theResourceName + " - Can handle: " + supportedResourceTypes);
}
return retVal;
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDao(Class<T> theResourceType) {
String resourceName = myCtx.getResourceDefinition(theResourceType).getName();
public <R extends IBaseResource> IFhirResourceDao<R> getResourceDao(Class<R> theResourceType) {
IFhirResourceDao<R> retVal = getResourceDaoIfExists(theResourceType);
Validate.notNull(retVal, "No DAO exists for resource type %s - Have: %s", theResourceType, myResourceNameToResourceDao);
return retVal;
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(Class<T> theResourceType) {
String resourceName = myContext.getResourceDefinition(theResourceType).getName();
return (IFhirResourceDao<T>) getResourceDao(resourceName);
}
private Map<String, IFhirResourceDao<?>> getResourceNameToResourceDao() {
Map<String, IFhirResourceDao<?>> retVal = myResourceNameToResourceDao;
if (retVal == null || retVal.isEmpty()) {
retVal = new HashMap<>();
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
for (IFhirResourceDao nextResourceDao : resourceDaos.values()) {
RuntimeResourceDefinition nextResourceDef = myCtx.getResourceDefinition(nextResourceDao.getResourceType());
retVal.put(nextResourceDef.getName(), nextResourceDao);
private void init() {
if (myResourceNameToResourceDao != null && !myResourceNameToResourceDao.isEmpty()) {
return;
}
myResourceNameToResourceDao = retVal;
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
initializeMaps(resourceDaos.values());
}
private void initializeMaps(Collection<IFhirResourceDao> theResourceDaos) {
myResourceNameToResourceDao = new HashMap<>();
for (IFhirResourceDao nextResourceDao : theResourceDaos) {
RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType());
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
}
}
public IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao retVal = getResourceDao(theClass);
if (retVal == null) {
List<String> supportedResourceNames = myResourceNameToResourceDao
.keySet()
.stream()
.map(t -> myContext.getResourceDefinition(t).getName())
.sorted()
.collect(Collectors.toList());
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + myContext.getResourceDefinition(theClass).getName() + " - Can handle: " + supportedResourceNames);
}
return retVal;
}
public void setResourceDaos(Collection<IFhirResourceDao> theResourceDaos) {
initializeMaps(theResourceDaos);
}
}

View File

@ -68,7 +68,6 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.TypedQuery;
@ -82,6 +81,8 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private DaoRegistry myDaoRegistry;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
@ -243,7 +244,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
requestDetails.setParameters(new HashMap<String, String[]>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = translateMatchUrl(params);
List<NameValuePair> parameters = myMatchUrlService.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.StringParam;
@ -41,7 +42,6 @@ import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hl7.fhir.dstu3.model.BaseResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
@ -65,11 +65,14 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
@Autowired
protected IForcedIdDao myForcedIdDao;
private Boolean ourDisabled;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private IdHelperService myIdHelperService;
private Boolean ourDisabled;
/**
* Constructor
*/
@ -225,7 +228,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
StringParam idParm = (StringParam) idParam;
idParamValue = idParm.getValue();
}
pid = BaseHapiFhirDao.translateForcedIdToPid(myDaoConfig, theResourceName, idParamValue, myForcedIdDao);
pid = myIdHelperService.translateForcedIdToPid(theResourceName, idParamValue);
}
Long referencingPid = pid;
@ -278,7 +281,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (contextParts.length != 3 || "Patient".equals(contextParts[0]) == false || "$everything".equals(contextParts[2]) == false) {
throw new InvalidRequestException("Invalid context: " + theContext);
}
Long pid = BaseHapiFhirDao.translateForcedIdToPid( myDaoConfig, contextParts[0], contextParts[1], myForcedIdDao);
Long pid = myIdHelperService.translateForcedIdToPid(contextParts[0], contextParts[1]);
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);

View File

@ -1,17 +1,13 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Collection;
import java.util.Set;
/*
* #%L
@ -41,10 +37,6 @@ public interface IDao {
FhirContext getContext();
RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName);
Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef);
/**
* Populate all of the runtime dependencies that a bundle provider requires in order to work
*/
@ -52,12 +44,9 @@ public interface IDao {
ISearchBuilder newSearchBuilder();
void populateFullTextFields(IBaseResource theResource, ResourceTable theEntity);
<R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType);
IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation);
<R extends IBaseResource> R toResource(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<ResourceTag> theTagList, boolean theForHistoryOperation);
ISearchParamRegistry getSearchParamRegistry();
}

View File

@ -42,7 +42,7 @@ public interface ISearchBuilder {
void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager,
FhirContext theContext, IDao theDao);
Set<Long> loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
Set<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
DateRangeParam theLastUpdated, String theSearchIdOrDescription);
/**

View File

@ -20,9 +20,11 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -53,4 +55,8 @@ public interface ISearchParamRegistry {
* Request that the cache be refreshed at the next convenient time (in a different thread)
*/
void requestRefresh();
RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName);
Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef);
}

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.trim;
public class LogicalReferenceHelper {
public static boolean isLogicalReference(DaoConfig myConfig, IIdType theId) {
Set<String> treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical();
if (treatReferencesAsLogical != null) {
for (String nextLogicalRef : treatReferencesAsLogical) {
nextLogicalRef = trim(nextLogicalRef);
if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') {
if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) {
return true;
}
} else {
if (theId.getValue().equals(nextLogicalRef)) {
return true;
}
}
}
}
return false;
}
}

View File

@ -0,0 +1,206 @@
package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.CoverageIgnore;
import com.google.common.collect.ArrayListMultimap;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
public class MatchUrlService {
@Autowired
private FhirContext myContext;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType);
SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef);
paramMap.setLoadSynchronous(true);
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
}
IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType);
if (dao == null) {
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
}
return dao.searchForIds(paramMap);
}
public SearchParameterMap translateMatchUrl(String
theMatchUrl, RuntimeResourceDefinition resourceDef) {
SearchParameterMap paramMap = new SearchParameterMap();
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
for (NameValuePair next : parameters) {
if (isBlank(next.getValue())) {
continue;
}
String paramName = next.getName();
String qualifier = null;
for (int i = 0; i < paramName.length(); i++) {
switch (paramName.charAt(i)) {
case '.':
case ':':
qualifier = paramName.substring(i);
paramName = paramName.substring(0, i);
i = Integer.MAX_VALUE - 1;
break;
}
}
QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
nameToParamLists.put(paramName, paramList);
}
for (String nextParamName : nameToParamLists.keySet()) {
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
if (paramList != null && paramList.size() > 0) {
if (paramList.size() > 2) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions");
} else {
DateRangeParam p1 = new DateRangeParam();
p1.setValuesAsQueryTokens(myContext, nextParamName, paramList);
paramMap.setLastUpdated(p1);
}
}
continue;
}
if (Constants.PARAM_HAS.equals(nextParamName)) {
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(myContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
paramMap.add(nextParamName, param);
continue;
}
if (Constants.PARAM_COUNT.equals(nextParamName)) {
if (paramList.size() > 0 && paramList.get(0).size() > 0) {
String intString = paramList.get(0).get(0);
try {
paramMap.setCount(Integer.parseInt(intString));
} catch (NumberFormatException e) {
throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString);
}
}
continue;
}
if (ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(nextParamName)) {
if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) {
throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier());
}
IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
type.setValuesAsQueryTokens(myContext, nextParamName, (paramList));
paramMap.add(nextParamName, type);
} else if (nextParamName.startsWith("_")) {
// ignore these since they aren't search params (e.g. _sort)
} else {
RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, nextParamName);
if (paramDef == null) {
throw new InvalidRequestException(
"Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName);
}
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(myContext, paramDef, nextParamName, paramList);
paramMap.add(nextParamName, param);
}
}
return paramMap;
}
public List<NameValuePair> translateMatchUrl(String theMatchUrl) {
List<NameValuePair> parameters;
String matchUrl = theMatchUrl;
int questionMarkIndex = matchUrl.indexOf('?');
if (questionMarkIndex != -1) {
matchUrl = matchUrl.substring(questionMarkIndex + 1);
}
matchUrl = matchUrl.replace("|", "%7C");
matchUrl = matchUrl.replace("=>=", "=%3E%3D");
matchUrl = matchUrl.replace("=<=", "=%3C%3D");
matchUrl = matchUrl.replace("=>", "=%3E");
matchUrl = matchUrl.replace("=<", "=%3C");
if (matchUrl.contains(" ")) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)");
}
parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&');
return parameters;
}
@CoverageIgnore
protected IQueryParameterAnd newInstanceAnd(String chain) {
IQueryParameterAnd type;
Class<? extends IQueryParameterAnd> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
@CoverageIgnore
public IQueryParameterType newInstanceType(String chain) {
IQueryParameterType type;
Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
}

View File

@ -0,0 +1,63 @@
package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.*;
import org.hl7.fhir.r4.model.BaseResource;
import java.util.*;
public class ResourceMetaParams {
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
public static final Map<String, Class<? extends IQueryParameterAnd<?>>> RESOURCE_META_AND_PARAMS;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
public static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
public static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
Map<String, Class<? extends IQueryParameterAnd<?>>> resourceMetaAndParams = new HashMap<String, Class<? extends IQueryParameterAnd<?>>>();
resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class);
resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class);
resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class);
resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class);
resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class);
resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<String>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
}
}

View File

@ -21,10 +21,11 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
@ -69,10 +70,15 @@ import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPre
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.math.BigDecimal;
@ -87,6 +93,8 @@ import static org.apache.commons.lang3.StringUtils.*;
* searches for resources
*/
@SuppressWarnings("JpaQlInspection")
@Component
@Scope("prototype")
public class SearchBuilder implements ISearchBuilder {
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
@ -97,26 +105,42 @@ public class SearchBuilder implements ISearchBuilder {
private static String ourLastHandlerThreadForUnitTest;
private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch;
private final DaoConfig myDaoConfig;
@Autowired
protected IResourceTagDao myResourceTagDao;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private IdHelperService myIdHelperService;
@Autowired(required = false)
private IFulltextSearchSvc myFulltextSearchSvc;
@Autowired
private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao;
private FhirContext myContext;
private EntityManager myEntityManager;
private IForcedIdDao myForcedIdDao;
private IFulltextSearchSvc myFulltextSearchSvc;
private Map<JoinKey, Join<?, ?>> myIndexJoins = Maps.newHashMap();
private SearchParameterMap myParams;
private ArrayList<Predicate> myPredicates;
private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
private String myResourceName;
private AbstractQuery<Long> myResourceTableQuery;
private Root<ResourceTable> myResourceTableRoot;
private Class<? extends IBaseResource> myResourceType;
private ISearchParamRegistry mySearchParamRegistry;
private String mySearchUuid;
private IHapiTerminologySvc myTerminologySvc;
private int myFetchSize;
private Integer myMaxResultsToFetch;
private Set<Long> myPidSet;
@ -124,22 +148,10 @@ public class SearchBuilder implements ISearchBuilder {
/**
* Constructor
*/
SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager,
IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao<?> theDao,
IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao,
IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry,
IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) {
myContext = theFhirContext;
myEntityManager = theEntityManager;
myFulltextSearchSvc = theFulltextSearchSvc;
SearchBuilder(BaseHapiFhirDao<?> theDao) {
myCallingDao = theDao;
myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches();
myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao;
myForcedIdDao = theForcedIdDao;
myTerminologySvc = theTerminologySvc;
mySearchParamRegistry = theSearchParamRegistry;
myResourceTagDao = theResourceTagDao;
myResourceSearchViewDao = theResourceViewDao;
myDaoConfig = theDao.getConfig();
myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches();
}
@Override
@ -220,24 +232,24 @@ public class SearchBuilder implements ISearchBuilder {
assert parameterName != null;
String paramName = parameterName.replaceAll("\\..*", "");
RuntimeSearchParam owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, paramName);
RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName);
if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName);
}
owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, owningParameter);
owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, owningParameter);
if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + owningParameter);
}
Class<? extends IBaseResource> resourceType = targetResourceDefinition.getImplementingClass();
Set<Long> match = myCallingDao.processMatchUrl(matchUrl, resourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(matchUrl, resourceType);
if (match.isEmpty()) {
// Pick a PID that can never match
match = Collections.singleton(-1L);
}
Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myIncomingResourceLinks", JoinType.LEFT);
Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT);
Predicate predicate = join.get("mySourceResourcePid").in(match);
myPredicates.add(predicate);
@ -373,7 +385,7 @@ public class SearchBuilder implements ISearchBuilder {
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
if (dt.hasBaseUrl()) {
if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
dt = dt.toUnqualified();
} else {
ourLog.debug("Searching for resource link with target URL: {}", dt.getValue());
@ -385,7 +397,7 @@ public class SearchBuilder implements ISearchBuilder {
List<Long> targetPid;
try {
targetPid = myCallingDao.translateForcedIdToPids(dt);
targetPid = myIdHelperService.translateForcedIdToPids(dt);
} catch (ResourceNotFoundException e) {
// Use a PID that will never exist
targetPid = Collections.singletonList(-1L);
@ -417,7 +429,7 @@ public class SearchBuilder implements ISearchBuilder {
if (resourceTypes.isEmpty()) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
RuntimeSearchParam searchParamByName = myCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (searchParamByName == null) {
throw new InternalErrorException("Could not find parameter " + theParamName);
}
@ -491,10 +503,10 @@ public class SearchBuilder implements ISearchBuilder {
chain = chain.substring(0, qualifierIndex);
}
boolean isMeta = BaseHapiFhirDao.RESOURCE_META_PARAMS.containsKey(chain);
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
RuntimeSearchParam param = null;
if (!isMeta) {
param = myCallingDao.getSearchParamByName(typeDef, chain);
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
if (param == null) {
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
continue;
@ -512,7 +524,7 @@ public class SearchBuilder implements ISearchBuilder {
chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
((ReferenceParam) chainValue).setChain(remainingChain);
} else if (isMeta) {
IQueryParameterType type = BaseHapiFhirDao.newInstanceType(chain);
IQueryParameterType type = myMatchUrlService.newInstanceType(chain);
type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
chainValue = type;
} else {
@ -1162,7 +1174,6 @@ public class SearchBuilder implements ISearchBuilder {
private Predicate createPredicateString(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamString> theFrom) {
String rawSearchTerm;
DaoConfig daoConfig = myCallingDao.getConfig();
if (theParameter instanceof TokenParam) {
TokenParam id = (TokenParam) theParameter;
if (!id.isText()) {
@ -1173,7 +1184,7 @@ public class SearchBuilder implements ISearchBuilder {
StringParam id = (StringParam) theParameter;
rawSearchTerm = id.getValue();
if (id.isContains()) {
if (!daoConfig.isAllowContainsSearches()) {
if (!myDaoConfig.isAllowContainsSearches()) {
throw new MethodNotAllowedException(":contains modifier is disabled on this server");
}
}
@ -1191,7 +1202,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myDontUseHashesForSearch) {
String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm);
if (myCallingDao.getConfig().isAllowContainsSearches()) {
if (myDaoConfig.isAllowContainsSearches()) {
if (theParameter instanceof StringParam) {
if (((StringParam) theParameter).isContains()) {
likeExpression = createLeftAndRightMatchLikeExpression(likeExpression);
@ -1230,13 +1241,13 @@ public class SearchBuilder implements ISearchBuilder {
String likeExpression;
if (theParameter instanceof StringParam &&
((StringParam) theParameter).isContains() &&
daoConfig.isAllowContainsSearches()) {
myDaoConfig.isAllowContainsSearches()) {
likeExpression = createLeftAndRightMatchLikeExpression(normalizedString);
} else {
likeExpression = createLeftMatchLikeExpression(normalizedString);
}
Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(daoConfig, theResourceName, theParamName, normalizedString);
Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig, theResourceName, theParamName, normalizedString);
Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash);
Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression);
return theBuilder.and(hashCode, singleCode);
@ -1473,7 +1484,7 @@ public class SearchBuilder implements ISearchBuilder {
* of parameters passed in
*/
ourLog.debug("Checking for unique index for query: {}", theParams.toNormalizedQueryString(myContext));
if (myCallingDao.getConfig().isUniqueIndexesEnabled()) {
if (myDaoConfig.isUniqueIndexesEnabled()) {
if (myParams.getIncludes().isEmpty()) {
if (myParams.getRevIncludes().isEmpty()) {
if (myParams.getEverythingMode() == null) {
@ -1600,7 +1611,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myParams.get(IAnyResource.SP_RES_ID) != null) {
StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
Long pid = BaseHapiFhirDao.translateForcedIdToPid(myCallingDao.getConfig(), myResourceName, idParm.getValue(), myForcedIdDao);
Long pid = myIdHelperService.translateForcedIdToPid(myResourceName, idParm.getValue());
if (myAlsoIncludePids == null) {
myAlsoIncludePids = new ArrayList<>(1);
}
@ -1677,7 +1688,7 @@ public class SearchBuilder implements ISearchBuilder {
}
private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From<?, ? extends ResourceLink> from) {
return createResourceLinkPathPredicate(myCallingDao, myContext, theParamName, from, theResourceName);
return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName);
}
/**
@ -1713,7 +1724,7 @@ public class SearchBuilder implements ISearchBuilder {
}
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theSort.getParamName());
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theSort.getParamName());
if (param == null) {
throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'");
}
@ -1807,7 +1818,7 @@ public class SearchBuilder implements ISearchBuilder {
String retVal = theSystem;
if (retVal == null) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (param != null) {
Set<String> valueSetUris = Sets.newHashSet();
for (String nextPath : param.getPathsSplit()) {
@ -1957,7 +1968,7 @@ public class SearchBuilder implements ISearchBuilder {
* so it can't be Collections.emptySet() or some such thing
*/
@Override
public HashSet<Long> loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
public HashSet<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription) {
if (theMatches.size() == 0) {
return new HashSet<>();
@ -2017,7 +2028,7 @@ public class SearchBuilder implements ISearchBuilder {
String paramName = nextInclude.getParamName();
if (isNotBlank(paramName)) {
param = theCallingDao.getSearchParamByName(def, paramName);
param = mySearchParamRegistry.getSearchParamByName(def, paramName);
} else {
param = null;
}
@ -2088,6 +2099,7 @@ public class SearchBuilder implements ISearchBuilder {
private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) {
myParams = theParams;
theParams.clean();
for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
String nextParamName = nextParamEntry.getKey();
List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue();
@ -2099,37 +2111,6 @@ public class SearchBuilder implements ISearchBuilder {
private void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
/*
* Filter out
*/
for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) {
List<? extends IQueryParameterType> nextOrList = theAndOrParams.get(andListIdx);
for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) {
IQueryParameterType nextOr = nextOrList.get(orListIdx);
boolean hasNoValue = false;
if (nextOr.getMissing() != null) {
continue;
}
if (nextOr instanceof QuantityParam) {
if (isBlank(((QuantityParam) nextOr).getValueAsString())) {
hasNoValue = true;
}
}
if (hasNoValue) {
ourLog.debug("Ignoring empty parameter: {}", theParamName);
nextOrList.remove(orListIdx);
orListIdx--;
}
}
if (nextOrList.isEmpty()) {
theAndOrParams.remove(andListIdx);
andListIdx--;
}
}
if (theAndOrParams.isEmpty()) {
return;
}
@ -2288,7 +2269,7 @@ public class SearchBuilder implements ISearchBuilder {
private int myCurrentOffset;
private ArrayList<Long> myCurrentPids;
private Long myNext;
private int myPageSize = myCallingDao.getConfig().getEverythingIncludesFetchPageSize();
private int myPageSize = myDaoConfig.getEverythingIncludesFetchPageSize();
IncludesIterator(Set<Long> thePidSet) {
myCurrentPids = new ArrayList<>(thePidSet);
@ -2316,7 +2297,7 @@ public class SearchBuilder implements ISearchBuilder {
myCurrentOffset = end;
Collection<Long> pidsToScan = myCurrentPids.subList(start, end);
Set<Include> includes = Collections.singleton(new Include("*", true));
Set<Long> newPids = loadIncludes(myCallingDao, myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid);
Set<Long> newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid);
myCurrentIterator = newPids.iterator();
}
@ -2368,7 +2349,7 @@ public class SearchBuilder implements ISearchBuilder {
// If we don't have a query yet, create one
if (myResultsIterator == null) {
if (myMaxResultsToFetch == null) {
myMaxResultsToFetch = myCallingDao.getConfig().getFetchSizeDefaultMaximum();
myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum();
}
final TypedQuery<Long> query = createQuery(mySort, myMaxResultsToFetch, false);
@ -2483,7 +2464,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myWrap == null) {
ourLog.debug("Searching for unique index matches over {} candidate query strings", myUniqueQueryStrings.size());
StopWatch sw = new StopWatch();
Collection<Long> resourcePids = myCallingDao.getResourceIndexedCompositeStringUniqueDao().findResourcePidsByQueryStrings(myUniqueQueryStrings);
Collection<Long> resourcePids = myResourceIndexedCompositeStringUniqueDao.findResourcePidsByQueryStrings(myUniqueQueryStrings);
ourLog.debug("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis());
myWrap = resourcePids.iterator();
}
@ -2621,10 +2602,10 @@ public class SearchBuilder implements ISearchBuilder {
return likeExpression.replace("%", "[%]") + "%";
}
private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
String theResourceType) {
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
List<String> path = param.getPathsSplit();
/*

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.StringUtils;
@ -17,6 +18,7 @@ import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
@ -40,6 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class SearchParameterMap extends LinkedHashMap<String, List<List<? extends IQueryParameterType>>> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class);
private static final long serialVersionUID = 1L;
@ -295,7 +298,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
* This method creates a URL query string representation of the parameters in this
* object, excluding the part before the parameters, e.g.
* <p>
* <code>?name=smith&_sort=Patient:family</code>
* <code>?name=smith&amp;_sort=Patient:family</code>
* </p>
* <p>
* This method <b>excludes</b> the <code>_count</code> parameter,
@ -336,6 +339,10 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
IQueryParameterType firstValue = nextValuesAnd.get(0);
b.append(UrlUtil.escapeUrlParam(nextKey));
if (nextKey.equals(Constants.PARAM_HAS)) {
b.append(':');
}
if (firstValue.getMissing() != null) {
b.append(Constants.PARAMQUALIFIER_MISSING);
b.append('=');
@ -433,6 +440,47 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
return b.toString();
}
public void clean() {
for (Map.Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : this.entrySet()) {
String nextParamName = nextParamEntry.getKey();
List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue();
clean(nextParamName, andOrParams);
}
}
/*
* Filter out
*/
private void clean(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) {
List<? extends IQueryParameterType> nextOrList = theAndOrParams.get(andListIdx);
for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) {
IQueryParameterType nextOr = nextOrList.get(orListIdx);
boolean hasNoValue = false;
if (nextOr.getMissing() != null) {
continue;
}
if (nextOr instanceof QuantityParam) {
if (isBlank(((QuantityParam) nextOr).getValueAsString())) {
hasNoValue = true;
}
}
if (hasNoValue) {
ourLog.debug("Ignoring empty parameter: {}", theParamName);
nextOrList.remove(orListIdx);
orListIdx--;
}
}
if (nextOrList.isEmpty()) {
theAndOrParams.remove(andListIdx);
andListIdx--;
}
}
}
public enum EverythingModeEnum {
/*
* Don't reorder! We rely on the ordinals

View File

@ -43,7 +43,10 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
@ -81,6 +84,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
@Autowired
private ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> myVersionAdapter;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private DaoRegistry myDaoRegistry;
private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) {
@ -390,7 +395,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
requestDetails.setParameters(new HashMap<>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = BaseHapiFhirDao.translateMatchUrl(params);
List<NameValuePair> parameters = myMatchUrlService.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
@ -481,8 +486,15 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
return myContext.getVersion().newIdType().setValue(theValue);
}
private Map<BUNDLEENTRY, ResourceTable> doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set<IIdType> theAllIds,
private Map<BUNDLEENTRY, ResourceTable> doTransactionWriteOperations(final ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap<BUNDLEENTRY, Integer> theOriginalRequestOrder, List<BUNDLEENTRY> theEntries, StopWatch theTransactionStopWatch) {
if (theRequestDetails != null) {
theRequestDetails.startDeferredOperationCallback();
}
try {
Set<String> deletedResources = new HashSet<>();
List<DeleteConflict> deleteConflicts = new ArrayList<>();
Map<BUNDLEENTRY, ResourceTable> entriesToProcess = new IdentityHashMap<>();
@ -759,6 +771,12 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
}
return entriesToProcess;
} finally {
if (theRequestDetails != null) {
theRequestDetails.stopDeferredRequestOperationCallbackAndRunDeferredItems();
}
}
}
private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) {

View File

@ -25,5 +25,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import ca.uhn.fhir.jpa.entity.ResourceLink;
public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
// nothing
}

View File

@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/*
* #%L
@ -55,4 +56,12 @@ public interface IResourceReindexJobDao extends JpaRepository<ResourceReindexJob
@Modifying
@Query("UPDATE ResourceReindexJobEntity j SET j.myThresholdLow = :low WHERE j.myId = :id")
void setThresholdLow(@Param("id") Long theId, @Param("low") Date theLow);
@Query("SELECT j.myReindexCount FROM ResourceReindexJobEntity j WHERE j.myId = :id")
Optional<Integer> getReindexCount(@Param("id") Long theId);
@Query("UPDATE ResourceReindexJobEntity j SET j.myReindexCount = :newCount WHERE j.myId = :id")
@Modifying
void setReindexCount(@Param("id") Long theId, @Param("newCount") int theNewCount);
}

View File

@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
@ -65,9 +64,6 @@ public class FhirResourceDaoCodeSystemDstu3 extends FhirResourceDaoDstu3<CodeSys
@Autowired
private ITermCodeSystemDao myCsDao;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private ValidationSupportChain myValidationSupport;

View File

@ -0,0 +1,103 @@
package ca.uhn.fhir.jpa.dao.index;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class IdHelperService {
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired(required = true)
private DaoConfig myDaoConfig;
public void delete(ForcedId forcedId) {
myForcedIdDao.delete(forcedId);
}
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
return translateForcedIdToPids(myDaoConfig, new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
}
public List<Long> translateForcedIdToPids(IIdType theId) {
return IdHelperService.translateForcedIdToPids(myDaoConfig, theId, myForcedIdDao);
}
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
Validate.isTrue(theId.hasIdPart());
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
return Collections.singletonList(theId.getIdPartAsLong());
} else {
List<ForcedId> forcedId;
if (theId.hasResourceType()) {
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
} else {
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
}
if (!forcedId.isEmpty()) {
List<Long> retVal = new ArrayList<>(forcedId.size());
for (ForcedId next : forcedId) {
retVal.add(next.getResourcePid());
}
return retVal;
} else {
throw new ResourceNotFoundException(theId);
}
}
}
public String translatePidIdToForcedId(String theResourceType, Long theId) {
ForcedId forcedId = myForcedIdDao.findByResourcePid(theId);
if (forcedId != null) {
return forcedId.getResourceType() + '/' + forcedId.getForcedId();
} else {
return theResourceType + '/' + theId.toString();
}
}
public static boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);
if (nextChar < '0' || nextChar > '9') {
return false;
}
}
return true;
}
}

View File

@ -1,58 +0,0 @@
package ca.uhn.fhir.jpa.dao.index;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.ISearchParamExtractor;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
public interface IndexingSupport {
public DaoConfig getConfig();
public ISearchParamExtractor getSearchParamExtractor();
public ISearchParamRegistry getSearchParamRegistry();
public FhirContext getContext();
public EntityManager getEntityManager();
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType);
public Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getResourceTypeToDao();
public boolean isLogicalReference(IIdType nextId);
public IForcedIdDao getForcedIdDao();
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType);
public Long translateForcedIdToPid(String theResourceName, String theResourceId);
public String toResourceName(Class<? extends IBaseResource> theResourceType);
public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao();
}

View File

@ -20,334 +20,78 @@ package ca.uhn.fhir.jpa.dao.index;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.compare;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.persistence.EntityManager;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Reference;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.PathAndRef;
import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import static org.apache.commons.lang3.StringUtils.*;
public class ResourceIndexedSearchParams {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceIndexedSearchParams.class);
// FIXME rename
private final IndexingSupport myIndexingService;
final Collection<ResourceIndexedSearchParamString> stringParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamToken> tokenParams = new HashSet<>();
final Collection<ResourceIndexedSearchParamNumber> numberParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamQuantity> quantityParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamDate> dateParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamUri> uriParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamCoords> coordsParams = new ArrayList<>();
private final Collection<ResourceIndexedSearchParamString> stringParams;
private final Collection<ResourceIndexedSearchParamToken> tokenParams;
private final Collection<ResourceIndexedSearchParamNumber> numberParams;
private final Collection<ResourceIndexedSearchParamQuantity> quantityParams;
private final Collection<ResourceIndexedSearchParamDate> dateParams;
private final Collection<ResourceIndexedSearchParamUri> uriParams;
private final Collection<ResourceIndexedSearchParamCoords> coordsParams;
private final Collection<ResourceIndexedCompositeStringUnique> compositeStringUniques;
private final Collection<ResourceLink> links;
final Collection<ResourceIndexedCompositeStringUnique> compositeStringUniques = new HashSet<>();
final Collection<ResourceLink> links = new HashSet<>();
final Set<String> populatedResourceLinkParameters = new HashSet<>();
private Set<String> populatedResourceLinkParameters = Collections.emptySet();
public ResourceIndexedSearchParams(IndexingSupport indexingService, ResourceTable theEntity) {
this.myIndexingService = indexingService;
public ResourceIndexedSearchParams() {
}
stringParams = new ArrayList<>();
public ResourceIndexedSearchParams(ResourceTable theEntity) {
if (theEntity.isParamsStringPopulated()) {
stringParams.addAll(theEntity.getParamsString());
}
tokenParams = new ArrayList<>();
if (theEntity.isParamsTokenPopulated()) {
tokenParams.addAll(theEntity.getParamsToken());
}
numberParams = new ArrayList<>();
if (theEntity.isParamsNumberPopulated()) {
numberParams.addAll(theEntity.getParamsNumber());
}
quantityParams = new ArrayList<>();
if (theEntity.isParamsQuantityPopulated()) {
quantityParams.addAll(theEntity.getParamsQuantity());
}
dateParams = new ArrayList<>();
if (theEntity.isParamsDatePopulated()) {
dateParams.addAll(theEntity.getParamsDate());
}
uriParams = new ArrayList<>();
if (theEntity.isParamsUriPopulated()) {
uriParams.addAll(theEntity.getParamsUri());
}
coordsParams = new ArrayList<>();
if (theEntity.isParamsCoordsPopulated()) {
coordsParams.addAll(theEntity.getParamsCoords());
}
links = new ArrayList<>();
if (theEntity.isHasLinks()) {
links.addAll(theEntity.getResourceLinks());
}
compositeStringUniques = new ArrayList<>();
if (theEntity.isParamsCompositeStringUniquePresent()) {
compositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique());
}
}
public ResourceIndexedSearchParams(IndexingSupport indexingService) {
this.myIndexingService = indexingService;
stringParams = Collections.emptySet();
tokenParams = Collections.emptySet();
numberParams = Collections.emptySet();
quantityParams = Collections.emptySet();
dateParams = Collections.emptySet();
uriParams = Collections.emptySet();
coordsParams = Collections.emptySet();
links = Collections.emptySet();
compositeStringUniques = Collections.emptySet();
}
public ResourceIndexedSearchParams(IndexingSupport indexingService, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) {
this.myIndexingService = indexingService;
stringParams = extractSearchParamStrings(theEntity, theResource);
numberParams = extractSearchParamNumber(theEntity, theResource);
quantityParams = extractSearchParamQuantity(theEntity, theResource);
dateParams = extractSearchParamDates(theEntity, theResource);
uriParams = extractSearchParamUri(theEntity, theResource);
coordsParams = extractSearchParamCoords(theEntity, theResource);
ourLog.trace("Storing date indexes: {}", dateParams);
tokenParams = new HashSet<>();
for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) {
if (next instanceof ResourceIndexedSearchParamToken) {
tokenParams.add((ResourceIndexedSearchParamToken) next);
} else {
stringParams.add((ResourceIndexedSearchParamString) next);
}
}
Set<Entry<String, RuntimeSearchParam>> activeSearchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(theEntity.getResourceType()).entrySet();
DaoConfig myConfig = indexingService.getConfig();
if (myConfig .getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.STRING, stringParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.DATE, dateParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.URI, uriParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams);
}
setUpdatedTime(stringParams, theUpdateTime);
setUpdatedTime(numberParams, theUpdateTime);
setUpdatedTime(quantityParams, theUpdateTime);
setUpdatedTime(dateParams, theUpdateTime);
setUpdatedTime(uriParams, theUpdateTime);
setUpdatedTime(coordsParams, theUpdateTime);
setUpdatedTime(tokenParams, theUpdateTime);
/*
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
* matching resource.
*/
if (myConfig.isAllowInlineMatchUrlReferences()) {
FhirTerser terser = myIndexingService.getContext().newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myIndexingService.getContext().getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
Set<Long> matches = myIndexingService.processMatchUrl(nextIdText, matchResourceType);
if (matches.isEmpty()) {
String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(msg);
}
if (matches.size() > 1) {
String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(msg);
}
Long next = matches.iterator().next();
String newId = translatePidIdToForcedId(resourceTypeString, next);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
nextRef.setReference(newId);
}
}
}
links = new HashSet<>();
populatedResourceLinkParameters = extractResourceLinks(theEntity, theResource, links, theUpdateTime);
/*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
*/
for (Iterator<ResourceLink> existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
ResourceLink nextExisting = existingLinkIter.next();
if (links.remove(nextExisting)) {
existingLinkIter.remove();
links.add(nextExisting);
}
}
/*
* Handle composites
*/
compositeStringUniques = extractCompositeStringUniques(theEntity, stringParams, tokenParams, numberParams, quantityParams, dateParams, uriParams, links);
}
public Collection<ResourceLink> getResourceLinks() {
return links;
}
protected Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamCoords(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamDates(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamNumber(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamQuantity(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamStrings(theEntity, theResource);
}
protected Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamTokens(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamUri(theEntity, theResource);
}
@SuppressWarnings("unchecked")
private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(ResourceTable theEntity, Set<Entry<String, RuntimeSearchParam>> activeSearchParams, RestSearchParameterTypeEnum type,
Collection<RT> paramCollection) {
for (Entry<String, RuntimeSearchParam> nextEntry : activeSearchParams) {
String nextParamName = nextEntry.getKey();
if (nextEntry.getValue().getParamType() == type) {
boolean haveParam = false;
for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
if (nextParam.getParamName().equals(nextParamName)) {
haveParam = true;
break;
}
}
if (!haveParam) {
BaseResourceIndexedSearchParam param;
switch (type) {
case DATE:
param = new ResourceIndexedSearchParamDate();
break;
case NUMBER:
param = new ResourceIndexedSearchParamNumber();
break;
case QUANTITY:
param = new ResourceIndexedSearchParamQuantity();
break;
case STRING:
param = new ResourceIndexedSearchParamString()
.setDaoConfig(myIndexingService.getConfig());
break;
case TOKEN:
param = new ResourceIndexedSearchParamToken();
break;
case URI:
param = new ResourceIndexedSearchParamUri();
break;
case COMPOSITE:
case HAS:
case REFERENCE:
default:
continue;
}
param.setResource(theEntity);
param.setMissing(true);
param.setParamName(nextParamName);
paramCollection.add((RT) param);
}
}
}
}
public void setParams(ResourceTable theEntity) {
public void setParamsOn(ResourceTable theEntity) {
theEntity.setParamsString(stringParams);
theEntity.setParamsStringPopulated(stringParams.isEmpty() == false);
theEntity.setParamsToken(tokenParams);
@ -367,88 +111,22 @@ public class ResourceIndexedSearchParams {
theEntity.setHasLinks(links.isEmpty() == false);
}
private Set<ResourceIndexedCompositeStringUnique> extractCompositeStringUniques(ResourceTable theEntity, Collection<ResourceIndexedSearchParamString> theStringParams, Collection<ResourceIndexedSearchParamToken> theTokenParams, Collection<ResourceIndexedSearchParamNumber> theNumberParams, Collection<ResourceIndexedSearchParamQuantity> theQuantityParams, Collection<ResourceIndexedSearchParamDate> theDateParams, Collection<ResourceIndexedSearchParamUri> theUriParams, Collection<ResourceLink> theLinks) {
Set<ResourceIndexedCompositeStringUnique> compositeStringUniques;
compositeStringUniques = new HashSet<>();
List<JpaRuntimeSearchParam> uniqueSearchParams = myIndexingService.getSearchParamRegistry().getActiveUniqueSearchParams(theEntity.getResourceType());
for (JpaRuntimeSearchParam next : uniqueSearchParams) {
List<List<String>> partsChoices = new ArrayList<>();
for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
Collection<ResourceLink> linksForCompositePart = null;
Collection<String> linksForCompositePartWantPaths = null;
switch (nextCompositeOf.getParamType()) {
case NUMBER:
paramsListForCompositePart = theNumberParams;
break;
case DATE:
paramsListForCompositePart = theDateParams;
break;
case STRING:
paramsListForCompositePart = theStringParams;
break;
case TOKEN:
paramsListForCompositePart = theTokenParams;
break;
case REFERENCE:
linksForCompositePart = theLinks;
linksForCompositePartWantPaths = new HashSet<>();
linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit());
break;
case QUANTITY:
paramsListForCompositePart = theQuantityParams;
break;
case URI:
paramsListForCompositePart = theUriParams;
break;
case COMPOSITE:
case HAS:
break;
public void setUpdatedTime(Date theUpdateTime) {
setUpdatedTime(stringParams, theUpdateTime);
setUpdatedTime(numberParams, theUpdateTime);
setUpdatedTime(quantityParams, theUpdateTime);
setUpdatedTime(dateParams, theUpdateTime);
setUpdatedTime(uriParams, theUpdateTime);
setUpdatedTime(coordsParams, theUpdateTime);
setUpdatedTime(tokenParams, theUpdateTime);
}
ArrayList<String> nextChoicesList = new ArrayList<>();
partsChoices.add(nextChoicesList);
String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
if (paramsListForCompositePart != null) {
for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
String value = nextParamAsClientParam.getValueAsQueryToken(myIndexingService.getContext());
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
if (linksForCompositePart != null) {
for (ResourceLink nextLink : linksForCompositePart) {
if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
nextSearchParam.setUpdated(theUpdateTime);
}
}
Set<String> queryStringsToPopulate = extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices);
for (String nextQueryString : queryStringsToPopulate) {
if (isNotBlank(nextQueryString)) {
compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
}
}
}
return compositeStringUniques;
}
/**
* This method is used to create a set of all possible combinations of
@ -531,342 +209,163 @@ public class ResourceIndexedSearchParams {
}
/**
* @return Returns a set containing all of the parameter names that
* were found to have a value
*/
@SuppressWarnings("unchecked")
protected Set<String> extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Collection<ResourceLink> theLinks, Date theUpdateTime) {
HashSet<String> retVal = new HashSet<>();
String resourceType = theEntity.getResourceType();
/*
* For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing..
*/
if (theResource instanceof IBaseBundle) {
return Collections.emptySet();
}
Map<String, RuntimeSearchParam> searchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(myIndexingService.toResourceName(theResource.getClass()));
for (RuntimeSearchParam nextSpDef : searchParams.values()) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
continue;
}
String nextPathsUnsplit = nextSpDef.getPath();
if (isBlank(nextPathsUnsplit)) {
continue;
}
boolean multiType = false;
if (nextPathsUnsplit.endsWith("[x]")) {
multiType = true;
}
List<PathAndRef> refs = myIndexingService.getSearchParamExtractor().extractResourceLinks(theResource, nextSpDef);
for (PathAndRef nextPathAndRef : refs) {
Object nextObject = nextPathAndRef.getRef();
/*
* A search parameter on an extension field that contains
* references should index those references
*/
if (nextObject instanceof IBaseExtension<?, ?>) {
nextObject = ((IBaseExtension<?, ?>) nextObject).getValue();
}
if (nextObject instanceof CanonicalType) {
nextObject = new Reference(((CanonicalType) nextObject).getValueAsString());
}
IIdType nextId;
if (nextObject instanceof IBaseReference) {
IBaseReference nextValue = (IBaseReference) nextObject;
if (nextValue.isEmpty()) {
continue;
}
nextId = nextValue.getReferenceElement();
/*
* This can only really happen if the DAO is being called
* programatically with a Bundle (not through the FHIR REST API)
* but Smile does this
*/
if (nextId.isEmpty() && nextValue.getResource() != null) {
nextId = nextValue.getResource().getIdElement();
}
if (nextId.isEmpty() || nextId.getValue().startsWith("#")) {
// This is a blank or contained resource reference
continue;
}
} else if (nextObject instanceof IBaseResource) {
nextId = ((IBaseResource) nextObject).getIdElement();
if (nextId == null || nextId.hasIdPart() == false) {
continue;
}
} else if (myIndexingService.getContext().getElementDefinition((Class<? extends IBase>) nextObject.getClass()).getName().equals("uri")) {
continue;
} else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) {
// Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
continue;
} else {
if (!multiType) {
if (nextSpDef.getName().equals("sourceuri")) {
continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI
}
throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
} else {
continue;
}
}
retVal.add(nextSpDef.getName());
if (myIndexingService.isLogicalReference(nextId)) {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
String baseUrl = nextId.getBaseUrl();
String typeString = nextId.getResourceType();
if (isBlank(typeString)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue());
}
RuntimeResourceDefinition resourceDefinition;
try {
resourceDefinition = myIndexingService.getContext().getResourceDefinition(typeString);
} catch (DataFormatException e) {
throw new InvalidRequestException(
"Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue());
}
if (isNotBlank(baseUrl)) {
if (!myIndexingService.getConfig().getTreatBaseUrlsAsLocal().contains(baseUrl) && !myIndexingService.getConfig().isAllowExternalReferences()) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue());
throw new InvalidRequestException(msg);
} else {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
}
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue());
}
IFhirResourceDao<?> dao = myIndexingService.getDao(type);
if (dao == null) {
StringBuilder b = new StringBuilder();
b.append("This server (version ");
b.append(myIndexingService.getContext().getVersion().getVersion());
b.append(") is not able to handle resources of type[");
b.append(nextId.getResourceType());
b.append("] - Valid resource types for this server: ");
b.append(myIndexingService.getResourceTypeToDao().keySet().toString());
throw new InvalidRequestException(b.toString());
}
Long valueOf;
try {
valueOf = myIndexingService.translateForcedIdToPid(typeString, id);
} catch (ResourceNotFoundException e) {
if (myIndexingService.getConfig().isEnforceReferentialIntegrityOnWrite() == false) {
continue;
}
RuntimeResourceDefinition missingResourceDef = myIndexingService.getContext().getResourceDefinition(type);
String resName = missingResourceDef.getName();
if (myIndexingService.getConfig().isAutoCreatePlaceholderReferenceTargets()) {
IBaseResource newResource = missingResourceDef.newInstance();
newResource.setId(resName + "/" + id);
IFhirResourceDao<IBaseResource> placeholderResourceDao = (IFhirResourceDao<IBaseResource>) myIndexingService.getDao(newResource.getClass());
ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
valueOf = placeholderResourceDao.update(newResource).getEntity().getId();
} else {
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
}
ResourceTable target = myIndexingService.getEntityManager().find(ResourceTable.class, valueOf);
RuntimeResourceDefinition targetResourceDef = myIndexingService.getContext().getResourceDefinition(type);
if (target == null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
if (!typeString.equals(target.getResourceType())) {
throw new UnprocessableEntityException(
"Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType());
}
if (target.getDeleted() != null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit);
}
if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) {
continue;
}
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime);
theLinks.add(resourceLink);
}
}
theEntity.setHasLinks(theLinks.size() > 0);
return retVal;
}
private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
nextSearchParam.setUpdated(theUpdateTime);
}
}
private String translatePidIdToForcedId(String theResourceType, Long theId) {
ForcedId forcedId = myIndexingService.getForcedIdDao().findByResourcePid(theId);
if (forcedId != null) {
return forcedId.getResourceType() + '/' + forcedId.getForcedId();
} else {
return theResourceType + '/' + theId.toString();
}
}
public void removeCommon(ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
EntityManager myEntityManager = myIndexingService.getEntityManager();
calculateHashes(stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, stringParams)) {
next.setDaoConfig(myIndexingService.getConfig());
myEntityManager .remove(next);
theEntity.getParamsString().remove(next);
}
for (ResourceIndexedSearchParamString next : removeCommon(stringParams, existingParams.stringParams)) {
myEntityManager.persist(next);
}
calculateHashes(tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, tokenParams)) {
myEntityManager.remove(next);
theEntity.getParamsToken().remove(next);
}
for (ResourceIndexedSearchParamToken next : removeCommon(tokenParams, existingParams.tokenParams)) {
myEntityManager.persist(next);
}
calculateHashes(numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, numberParams)) {
myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next);
}
for (ResourceIndexedSearchParamNumber next : removeCommon(numberParams, existingParams.numberParams)) {
myEntityManager.persist(next);
}
calculateHashes(quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, quantityParams)) {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
}
for (ResourceIndexedSearchParamQuantity next : removeCommon(quantityParams, existingParams.quantityParams)) {
myEntityManager.persist(next);
}
// Store date SP's
calculateHashes(dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, dateParams)) {
myEntityManager.remove(next);
theEntity.getParamsDate().remove(next);
}
for (ResourceIndexedSearchParamDate next : removeCommon(dateParams, existingParams.dateParams)) {
myEntityManager.persist(next);
}
// Store URI SP's
calculateHashes(uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, uriParams)) {
myEntityManager.remove(next);
theEntity.getParamsUri().remove(next);
}
for (ResourceIndexedSearchParamUri next : removeCommon(uriParams, existingParams.uriParams)) {
myEntityManager.persist(next);
}
// Store Coords SP's
calculateHashes(coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, coordsParams)) {
myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next);
}
for (ResourceIndexedSearchParamCoords next : removeCommon(coordsParams, existingParams.coordsParams)) {
myEntityManager.persist(next);
}
// Store resource links
for (ResourceLink next : removeCommon(existingParams.links, links)) {
myEntityManager.remove(next);
theEntity.getResourceLinks().remove(next);
}
for (ResourceLink next : removeCommon(links, existingParams.links)) {
myEntityManager.persist(next);
}
// make sure links are indexed
theEntity.setResourceLinks(links);
// Store composite string uniques
if (myIndexingService.getConfig().isUniqueIndexesEnabled()) {
for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, compositeStringUniques)) {
ourLog.debug("Removing unique index: {}", next);
myEntityManager.remove(next);
theEntity.getParamsCompositeStringUnique().remove(next);
}
for (ResourceIndexedCompositeStringUnique next : removeCommon(compositeStringUniques, existingParams.compositeStringUniques)) {
if (myIndexingService.getConfig().isUniqueIndexesCheckedBeforeSave()) {
ResourceIndexedCompositeStringUnique existing = myIndexingService.getResourceIndexedCompositeStringUniqueDao().findByQueryString(next.getIndexString());
if (existing != null) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new PreconditionFailedException(msg);
}
}
ourLog.debug("Persisting unique index: {}", next);
myEntityManager.persist(next);
}
}
}
private void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
for (BaseResourceIndexedSearchParam next : theStringParams) {
next.calculateHashes();
}
}
private <T> Collection<T> removeCommon(Collection<T> theInput, Collection<T> theToRemove) {
assert theInput != theToRemove;
if (theInput.isEmpty()) {
return theInput;
}
ArrayList<T> retVal = new ArrayList<>(theInput);
retVal.removeAll(theToRemove);
return retVal;
}
public Set<String> getPopulatedResourceLinkParameters() {
return populatedResourceLinkParameters;
}
public boolean matchParam(String theResourceName, String theParamName, RuntimeSearchParam paramDef, IQueryParameterType theParam) {
if (paramDef == null) {
return false;
}
Collection<? extends BaseResourceIndexedSearchParam> resourceParams;
switch (paramDef.getParamType()) {
case TOKEN:
resourceParams = tokenParams;
break;
case QUANTITY:
resourceParams = quantityParams;
break;
case STRING:
resourceParams = stringParams;
break;
case NUMBER:
resourceParams = numberParams;
break;
case URI:
resourceParams = uriParams;
break;
case DATE:
resourceParams = dateParams;
break;
case REFERENCE:
return matchResourceLinks(theResourceName, theParamName, theParam);
case COMPOSITE:
case HAS:
case SPECIAL:
default:
resourceParams = null;
}
if (resourceParams == null) {
return false;
}
Predicate<BaseResourceIndexedSearchParam> namedParamPredicate = param ->
param.getParamName().equalsIgnoreCase(theParamName) &&
param.matches(theParam);
return resourceParams.stream().anyMatch(namedParamPredicate);
}
private boolean matchResourceLinks(String theResourceName, String theParamName, IQueryParameterType theParam) {
ReferenceParam reference = (ReferenceParam)theParam;
Predicate<ResourceLink> namedParamPredicate = resourceLink ->
resourceLinkMatches(theResourceName, resourceLink, theParamName)
&& resourceIdMatches(resourceLink, reference);
return links.stream().anyMatch(namedParamPredicate);
}
private boolean resourceIdMatches(ResourceLink theResourceLink, ReferenceParam theReference) {
ResourceTable target = theResourceLink.getTargetResource();
IdDt idDt = target.getIdDt();
if (idDt.isIdPartValidLong()) {
return theReference.getIdPartAsLong() == idDt.getIdPartAsLong();
} else {
ForcedId forcedId = target.getForcedId();
if (forcedId != null) {
return forcedId.getForcedId().equals(theReference.getValue());
} else {
return false;
}
}
}
private boolean resourceLinkMatches(String theResourceName, ResourceLink theResourceLink, String theParamName) {
return theResourceLink.getTargetResource().getResourceType().equalsIgnoreCase(theParamName) ||
theResourceLink.getSourcePath().equalsIgnoreCase(theResourceName+"."+theParamName);
}
@Override
public String toString() {
return "ResourceIndexedSearchParams{" +
"stringParams=" + stringParams +
", tokenParams=" + tokenParams +
", numberParams=" + numberParams +
", quantityParams=" + quantityParams +
", dateParams=" + dateParams +
", uriParams=" + uriParams +
", coordsParams=" + coordsParams +
", compositeStringUniques=" + compositeStringUniques +
", links=" + links +
'}';
}
void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set<Entry<String, RuntimeSearchParam>> theActiveSearchParams) {
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.STRING, stringParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.DATE, dateParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.URI, uriParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams);
}
@SuppressWarnings("unchecked")
private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams, RestSearchParameterTypeEnum type,
Collection<RT> paramCollection) {
for (Map.Entry<String, RuntimeSearchParam> nextEntry : activeSearchParams) {
String nextParamName = nextEntry.getKey();
if (nextEntry.getValue().getParamType() == type) {
boolean haveParam = false;
for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
if (nextParam.getParamName().equals(nextParamName)) {
haveParam = true;
break;
}
}
if (!haveParam) {
BaseResourceIndexedSearchParam param;
switch (type) {
case DATE:
param = new ResourceIndexedSearchParamDate();
break;
case NUMBER:
param = new ResourceIndexedSearchParamNumber();
break;
case QUANTITY:
param = new ResourceIndexedSearchParamQuantity();
break;
case STRING:
param = new ResourceIndexedSearchParamString()
.setDaoConfig(theDaoConfig);
break;
case TOKEN:
param = new ResourceIndexedSearchParamToken();
break;
case URI:
param = new ResourceIndexedSearchParamUri();
break;
case COMPOSITE:
case HAS:
case REFERENCE:
default:
continue;
}
param.setResource(theEntity);
param.setMissing(true);
param.setParamName(nextParamName);
paramCollection.add((RT) param);
}
}
}
}
}

View File

@ -0,0 +1,604 @@
package ca.uhn.fhir.jpa.dao.index;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
@Lazy
public class SearchParamExtractorService {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class);
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private FhirContext myContext;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@Autowired
private ISearchParamExtractor mySearchParamExtractor;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) {
extractFromResource(theParams, theEntity, theResource);
Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
theParams.findMissingSearchParams(myDaoConfig, theEntity, activeSearchParams);
}
theParams.setUpdatedTime(theUpdateTime);
extractInlineReferences(theResource);
extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, true);
/*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
*/
for (Iterator<ResourceLink> existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
ResourceLink nextExisting = existingLinkIter.next();
if (theParams.links.remove(nextExisting)) {
existingLinkIter.remove();
theParams.links.add(nextExisting);
}
}
/*
* Handle composites
*/
extractCompositeStringUniques(theEntity, theParams);
}
public void extractFromResource(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource) {
theParams.stringParams.addAll(extractSearchParamStrings(theEntity, theResource));
theParams.numberParams.addAll(extractSearchParamNumber(theEntity, theResource));
theParams.quantityParams.addAll(extractSearchParamQuantity(theEntity, theResource));
theParams.dateParams.addAll(extractSearchParamDates(theEntity, theResource));
theParams.uriParams.addAll(extractSearchParamUri(theEntity, theResource));
theParams.coordsParams.addAll(extractSearchParamCoords(theEntity, theResource));
ourLog.trace("Storing date indexes: {}", theParams.dateParams);
for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) {
if (next instanceof ResourceIndexedSearchParamToken) {
theParams.tokenParams.add((ResourceIndexedSearchParamToken) next);
} else {
theParams.stringParams.add((ResourceIndexedSearchParamString) next);
}
}
}
/**
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
* matching resource.
*/
public void extractInlineReferences(IBaseResource theResource) {
if (!myDaoConfig.isAllowInlineMatchUrlReferences()) {
return;
}
FhirTerser terser = myContext.newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
Set<Long> matches = myMatchUrlService.processMatchUrl(nextIdText, matchResourceType);
if (matches.isEmpty()) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(msg);
}
if (matches.size() > 1) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(msg);
}
Long next = matches.iterator().next();
String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, next);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
nextRef.setReference(newId);
}
}
}
protected Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamNumber(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamQuantity(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamStrings(theEntity, theResource);
}
protected Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamTokens(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource);
}
private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
String resourceType = theEntity.getResourceType();
List<JpaRuntimeSearchParam> uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(resourceType);
for (JpaRuntimeSearchParam next : uniqueSearchParams) {
List<List<String>> partsChoices = new ArrayList<>();
for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
Collection<ResourceLink> linksForCompositePart = null;
Collection<String> linksForCompositePartWantPaths = null;
switch (nextCompositeOf.getParamType()) {
case NUMBER:
paramsListForCompositePart = theParams.numberParams;
break;
case DATE:
paramsListForCompositePart = theParams.dateParams;
break;
case STRING:
paramsListForCompositePart = theParams.stringParams;
break;
case TOKEN:
paramsListForCompositePart = theParams.tokenParams;
break;
case REFERENCE:
linksForCompositePart = theParams.links;
linksForCompositePartWantPaths = new HashSet<>();
linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit());
break;
case QUANTITY:
paramsListForCompositePart = theParams.quantityParams;
break;
case URI:
paramsListForCompositePart = theParams.uriParams;
break;
case COMPOSITE:
case HAS:
break;
}
ArrayList<String> nextChoicesList = new ArrayList<>();
partsChoices.add(nextChoicesList);
String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
if (paramsListForCompositePart != null) {
for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
String value = nextParamAsClientParam.getValueAsQueryToken(myContext);
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
if (linksForCompositePart != null) {
for (ResourceLink nextLink : linksForCompositePart) {
if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
}
Set<String> queryStringsToPopulate = theParams.extractCompositeStringUniquesValueChains(resourceType, partsChoices);
for (String nextQueryString : queryStringsToPopulate) {
if (isNotBlank(nextQueryString)) {
theParams.compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
}
}
}
}
@SuppressWarnings("unchecked")
public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, boolean lookUpReferencesInDatabase) {
String resourceType = theEntity.getResourceType();
/*
* For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing..
*/
if (theResource instanceof IBaseBundle) {
return;
}
Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass()));
for (RuntimeSearchParam nextSpDef : searchParams.values()) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
continue;
}
String nextPathsUnsplit = nextSpDef.getPath();
if (isBlank(nextPathsUnsplit)) {
continue;
}
boolean multiType = false;
if (nextPathsUnsplit.endsWith("[x]")) {
multiType = true;
}
List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef);
for (PathAndRef nextPathAndRef : refs) {
Object nextObject = nextPathAndRef.getRef();
/*
* A search parameter on an extension field that contains
* references should index those references
*/
if (nextObject instanceof IBaseExtension<?, ?>) {
nextObject = ((IBaseExtension<?, ?>) nextObject).getValue();
}
if (nextObject instanceof CanonicalType) {
nextObject = new Reference(((CanonicalType) nextObject).getValueAsString());
}
IIdType nextId;
if (nextObject instanceof IBaseReference) {
IBaseReference nextValue = (IBaseReference) nextObject;
if (nextValue.isEmpty()) {
continue;
}
nextId = nextValue.getReferenceElement();
/*
* This can only really happen if the DAO is being called
* programatically with a Bundle (not through the FHIR REST API)
* but Smile does this
*/
if (nextId.isEmpty() && nextValue.getResource() != null) {
nextId = nextValue.getResource().getIdElement();
}
if (nextId.isEmpty() || nextId.getValue().startsWith("#")) {
// This is a blank or contained resource reference
continue;
}
} else if (nextObject instanceof IBaseResource) {
nextId = ((IBaseResource) nextObject).getIdElement();
if (nextId == null || nextId.hasIdPart() == false) {
continue;
}
} else if (myContext.getElementDefinition((Class<? extends IBase>) nextObject.getClass()).getName().equals("uri")) {
continue;
} else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) {
// Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
continue;
} else {
if (!multiType) {
if (nextSpDef.getName().equals("sourceuri")) {
continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI
}
throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
} else {
continue;
}
}
theParams.populatedResourceLinkParameters.add(nextSpDef.getName());
if (LogicalReferenceHelper.isLogicalReference(myDaoConfig, nextId)) {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theParams.links.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
String baseUrl = nextId.getBaseUrl();
String typeString = nextId.getResourceType();
if (isBlank(typeString)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue());
}
RuntimeResourceDefinition resourceDefinition;
try {
resourceDefinition = myContext.getResourceDefinition(typeString);
} catch (DataFormatException e) {
throw new InvalidRequestException(
"Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue());
}
if (isNotBlank(baseUrl)) {
if (!myDaoConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myDaoConfig.isAllowExternalReferences()) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue());
throw new InvalidRequestException(msg);
} else {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theParams.links.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
}
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue());
}
myDaoRegistry.getDaoOrThrowException(type);
ResourceTable target;
if (lookUpReferencesInDatabase) {
Long valueOf;
try {
valueOf = myIdHelperService.translateForcedIdToPid(typeString, id);
} catch (ResourceNotFoundException e) {
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
continue;
}
RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type);
String resName = missingResourceDef.getName();
if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
IBaseResource newResource = missingResourceDef.newInstance();
newResource.setId(resName + "/" + id);
IFhirResourceDao<IBaseResource> placeholderResourceDao = (IFhirResourceDao<IBaseResource>) myDaoRegistry.getResourceDao(newResource.getClass());
ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
valueOf = placeholderResourceDao.update(newResource).getEntity().getId();
} else {
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
}
target = myEntityManager.find(ResourceTable.class, valueOf);
RuntimeResourceDefinition targetResourceDef = myContext.getResourceDefinition(type);
if (target == null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
if (!typeString.equals(target.getResourceType())) {
throw new UnprocessableEntityException(
"Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType());
}
if (target.getDeleted() != null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit);
}
if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) {
continue;
}
} else {
target = new ResourceTable();
target.setResourceType(typeString);
if (nextId.isIdPartValidLong()) {
target.setId(nextId.getIdPartAsLong());
} else {
ForcedId forcedId = new ForcedId();
forcedId.setForcedId(id);
target.setForcedId(forcedId);
}
}
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime);
theParams.links.add(resourceLink);
}
}
theEntity.setHasLinks(theParams.links.size() > 0);
}
public String toResourceName(Class<? extends IBaseResource> theResourceType) {
return myContext.getResourceDefinition(theResourceType).getName();
}
public void removeCommon(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
theParams.calculateHashes(theParams.stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, theParams.stringParams)) {
next.setDaoConfig(myDaoConfig);
myEntityManager.remove(next);
theEntity.getParamsString().remove(next);
}
for (ResourceIndexedSearchParamString next : removeCommon(theParams.stringParams, existingParams.stringParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, theParams.tokenParams)) {
myEntityManager.remove(next);
theEntity.getParamsToken().remove(next);
}
for (ResourceIndexedSearchParamToken next : removeCommon(theParams.tokenParams, existingParams.tokenParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, theParams.numberParams)) {
myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next);
}
for (ResourceIndexedSearchParamNumber next : removeCommon(theParams.numberParams, existingParams.numberParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, theParams.quantityParams)) {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
}
for (ResourceIndexedSearchParamQuantity next : removeCommon(theParams.quantityParams, existingParams.quantityParams)) {
myEntityManager.persist(next);
}
// Store date SP's
theParams.calculateHashes(theParams.dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, theParams.dateParams)) {
myEntityManager.remove(next);
theEntity.getParamsDate().remove(next);
}
for (ResourceIndexedSearchParamDate next : removeCommon(theParams.dateParams, existingParams.dateParams)) {
myEntityManager.persist(next);
}
// Store URI SP's
theParams.calculateHashes(theParams.uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, theParams.uriParams)) {
myEntityManager.remove(next);
theEntity.getParamsUri().remove(next);
}
for (ResourceIndexedSearchParamUri next : removeCommon(theParams.uriParams, existingParams.uriParams)) {
myEntityManager.persist(next);
}
// Store Coords SP's
theParams.calculateHashes(theParams.coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, theParams.coordsParams)) {
myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next);
}
for (ResourceIndexedSearchParamCoords next : removeCommon(theParams.coordsParams, existingParams.coordsParams)) {
myEntityManager.persist(next);
}
// Store resource links
for (ResourceLink next : removeCommon(existingParams.links, theParams.links)) {
myEntityManager.remove(next);
theEntity.getResourceLinks().remove(next);
}
for (ResourceLink next : removeCommon(theParams.links, existingParams.links)) {
myEntityManager.persist(next);
}
// make sure links are indexed
theEntity.setResourceLinks(theParams.links);
// Store composite string uniques
if (myDaoConfig.isUniqueIndexesEnabled()) {
for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, theParams.compositeStringUniques)) {
ourLog.debug("Removing unique index: {}", next);
myEntityManager.remove(next);
theEntity.getParamsCompositeStringUnique().remove(next);
}
for (ResourceIndexedCompositeStringUnique next : removeCommon(theParams.compositeStringUniques, existingParams.compositeStringUniques)) {
if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) {
ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
if (existing != null) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new PreconditionFailedException(msg);
}
}
ourLog.debug("Persisting unique index: {}", next);
myEntityManager.persist(next);
}
}
}
private <T> Collection<T> removeCommon(Collection<T> theInput, Collection<T> theToRemove) {
assert theInput != theToRemove;
if (theInput.isEmpty()) {
return theInput;
}
ArrayList<T> retVal = new ArrayList<>(theInput);
retVal.removeAll(theToRemove);
return retVal;
}
}

View File

@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
@ -64,8 +63,6 @@ public class FhirResourceDaoCodeSystemR4 extends FhirResourceDaoR4<CodeSystem> i
@Autowired
private ITermCodeSystemDao myCsDao;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private ValidationSupportChain myValidationSupport;
@Override

View File

@ -156,4 +156,7 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable {
return hashCode.asLong();
}
public boolean matches(IQueryParameterType theParam) {
throw new UnsupportedOperationException("No parameter matcher for "+theParam);
}
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -184,4 +185,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
b.append("valueHigh", new InstantDt(getValueHigh()));
return b.build();
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof DateParam)) {
return false;
}
DateParam date = (DateParam) theParam;
DateRangeParam range = new DateRangeParam(date);
Date lowerBound = range.getLowerBoundAsInstant();
Date upperBound = range.getUpperBoundAsInstant();
if (lowerBound == null && upperBound == null) {
// should never happen
return false;
}
boolean result = true;
if (lowerBound != null) {
result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
result &= (myValueHigh.after(lowerBound) || myValueHigh.equals(lowerBound));
}
if (upperBound != null) {
result &= (myValueLow.before(upperBound) || myValueLow.equals(upperBound));
result &= (myValueHigh.before(upperBound) || myValueHigh.equals(upperBound));
}
return result;
}
}

View File

@ -149,4 +149,13 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP
b.append("value", getValue());
return b.build();
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof NumberParam)) {
return false;
}
NumberParam number = (NumberParam)theParam;
return getValue().equals(number.getValue());
}
}

View File

@ -235,4 +235,37 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc
return hash(theResourceType, theParamName, theUnits);
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof QuantityParam)) {
return false;
}
QuantityParam quantity = (QuantityParam)theParam;
boolean retval = false;
// Only match on system if it wasn't specified
if (quantity.getSystem() == null && quantity.getUnits() == null) {
if (getValue().equals(quantity.getValue())) {
retval = true;
}
} else if (quantity.getSystem() == null) {
if (getUnits().equalsIgnoreCase(quantity.getUnits()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
} else if (quantity.getUnits() == null) {
if (getSystem().equalsIgnoreCase(quantity.getSystem()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
} else {
if (getSystem().equalsIgnoreCase(quantity.getSystem()) &&
getUnits().equalsIgnoreCase(quantity.getUnits()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
}
return retval;
}
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity;
* #L%
*/
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.StringParam;
@ -294,4 +295,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
return hash(theResourceType, theParamName, left(theValueNormalized, hashPrefixLength));
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof StringParam)) {
return false;
}
StringParam string = (StringParam)theParam;
String normalizedString = BaseHapiFhirDao.normalizeString(string.getValue());
return getValueNormalized().startsWith(normalizedString);
}
}

View File

@ -251,4 +251,29 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa
public static long calculateHashValue(String theResourceType, String theParamName, String theValue) {
return hash(theResourceType, theParamName, trim(theValue));
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof TokenParam)) {
return false;
}
TokenParam token = (TokenParam)theParam;
boolean retval = false;
// Only match on system if it wasn't specified
if (token.getSystem() == null || token.getSystem().isEmpty()) {
if (getValue().equalsIgnoreCase(token.getValue())) {
retval = true;
}
} else if (token.getValue() == null || token.getValue().isEmpty()) {
if (token.getSystem().equalsIgnoreCase(getSystem())) {
retval = true;
}
} else {
if (token.getSystem().equalsIgnoreCase(getSystem()) &&
getValue().equalsIgnoreCase(token.getValue())) {
retval = true;
}
}
return retval;
}
}

View File

@ -179,4 +179,13 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
return hash(theResourceType, theParamName, theUri);
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof UriParam)) {
return false;
}
UriParam uri = (UriParam)theParam;
return getUri().equalsIgnoreCase(uri.getValueNotNull());
}
}

View File

@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.entity;
*/
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.persistence.*;
import java.io.Serializable;
@ -53,6 +55,16 @@ public class ResourceReindexJobEntity implements Serializable {
@Column(name = "SUSPENDED_UNTIL", nullable = true)
@Temporal(TemporalType.TIMESTAMP)
private Date mySuspendedUntil;
@Column(name = "REINDEX_COUNT", nullable = true)
private Integer myReindexCount;
public Integer getReindexCount() {
return myReindexCount;
}
public void setReindexCount(Integer theReindexCount) {
myReindexCount = theReindexCount;
}
public Date getSuspendedUntil() {
return mySuspendedUntil;
@ -110,4 +122,20 @@ public class ResourceReindexJobEntity implements Serializable {
public void setDeleted(boolean theDeleted) {
myDeleted = theDeleted;
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("id", myId)
.append("resourceType", myResourceType)
.append("thresholdLow", myThresholdLow)
.append("thresholdHigh", myThresholdHigh);
if (myDeleted) {
b.append("deleted", myDeleted);
}
if (mySuspendedUntil != null) {
b.append("suspendedUntil", mySuspendedUntil);
}
return b.toString();
}
}

View File

@ -87,10 +87,6 @@ public class ResourceTable extends BaseHasResource implements Serializable {
@Column(name = "RES_ID")
private Long myId;
@OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false)
@OptimisticLock(excluded = true)
private Collection<ResourceLink> myIncomingResourceLinks;
@Column(name = "SP_INDEX_STATUS", nullable = true)
@OptimisticLock(excluded = true)
private Long myIndexStatus;
@ -181,31 +177,36 @@ public class ResourceTable extends BaseHasResource implements Serializable {
@OptimisticLock(excluded = true)
private Collection<ResourceIndexedCompositeStringUnique> myParamsCompositeStringUnique;
@IndexedEmbedded
@OneToMany(mappedBy = "mySourceResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false)
@IndexedEmbedded()
@OptimisticLock(excluded = true)
private Collection<ResourceLink> myResourceLinks;
@OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false)
@OptimisticLock(excluded = true)
private Collection<ResourceLink> myResourceLinksAsTarget;
@Column(name = "RES_TYPE", length = RESTYPE_LEN)
@Field
@OptimisticLock(excluded = true)
private String myResourceType;
@OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@OptimisticLock(excluded = true)
private Collection<SearchParamPresent> mySearchParamPresents;
@OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@OptimisticLock(excluded = true)
private Set<ResourceTag> myTags;
@Transient
private transient boolean myUnchangedInCurrentOperation;
@Version
@Column(name = "RES_VER")
private long myVersion;
public Collection<ResourceLink> getResourceLinksAsTarget() {
if (myResourceLinksAsTarget == null) {
myResourceLinksAsTarget = new ArrayList<>();
}
return myResourceLinksAsTarget;
}
@Override
public ResourceTag addTag(TagDefinition theTag) {
for (ResourceTag next : getTags()) {

View File

@ -64,7 +64,7 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
for (Argument nextArgument : theSearchParams) {
RuntimeSearchParam searchParam = getSearchParamByName(typeDef, nextArgument.getName());
RuntimeSearchParam searchParam = mySearchParamRegistry.getSearchParamByName(typeDef, nextArgument.getName());
for (Value nextValue : nextArgument.getValues()) {
String value = nextValue.getValue();

View File

@ -21,133 +21,36 @@ package ca.uhn.fhir.jpa.provider;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.ISubscriptionTriggeringSvc;
import ca.uhn.fhir.jpa.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.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseParameters;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SubscriptionTriggeringProvider implements IResourceProvider, ApplicationContextAware {
import java.util.List;
public class SubscriptionTriggeringProvider implements IResourceProvider {
public static final String RESOURCE_ID = "resourceId";
public static final int DEFAULT_MAX_SUBMIT = 10000;
public static final String SEARCH_URL = "searchUrl";
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class);
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
@Autowired
private FhirContext myFhirContext;
@Autowired
private DaoRegistry myDaoRegistry;
private List<BaseSubscriptionInterceptor<?>> mySubscriptionInterceptorList;
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
@Autowired
private ISearchCoordinatorSvc mySearchCoordinatorSvc;
private ApplicationContext myAppCtx;
private ExecutorService myExecutorService;
private ISubscriptionTriggeringSvc mySubscriptionTriggeringSvc;
/**
* Sets the maximum number of resources that will be submitted in a single pass
*/
public void setMaxSubmitPerPass(Integer theMaxSubmitPerPass) {
Integer maxSubmitPerPass = theMaxSubmitPerPass;
if (maxSubmitPerPass == null) {
maxSubmitPerPass = DEFAULT_MAX_SUBMIT;
}
Validate.isTrue(maxSubmitPerPass > 0, "theMaxSubmitPerPass must be > 0");
myMaxSubmitPerPass = maxSubmitPerPass;
}
@SuppressWarnings("unchecked")
@PostConstruct
public void start() {
mySubscriptionInterceptorList = ObjectUtils.defaultIfNull(mySubscriptionInterceptorList, Collections.emptyList());
mySubscriptionInterceptorList = new ArrayList<>();
Collection values1 = myAppCtx.getBeansOfType(BaseSubscriptionInterceptor.class).values();
Collection<BaseSubscriptionInterceptor<?>> values = (Collection<BaseSubscriptionInterceptor<?>>) values1;
mySubscriptionInterceptorList.addAll(values);
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("SubscriptionTriggering-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
ourLog.info("Note: Subscription triggering queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
StopWatch sw = new StopWatch();
try {
executorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
}
};
myExecutorService = new ThreadPoolExecutor(
0,
10,
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
rejectedExecutionHandler);
}
@Operation(name = JpaConstants.OPERATION_TRIGGER_SUBSCRIPTION)
public IBaseParameters triggerSubscription(
@OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List<UriParam> theResourceIds,
@OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List<StringParam> theSearchUrls
) {
return doTriggerSubscription(theResourceIds, theSearchUrls, null);
return mySubscriptionTriggeringSvc.triggerSubscription(theResourceIds, theSearchUrls, null);
}
@Operation(name = JpaConstants.OPERATION_TRIGGER_SUBSCRIPTION)
@ -156,331 +59,13 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
@OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List<UriParam> theResourceIds,
@OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List<StringParam> theSearchUrls
) {
// Throw a 404 if the subscription doesn't exist
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
IIdType subscriptionId = theSubscriptionId;
if (subscriptionId.hasResourceType() == false) {
subscriptionId = subscriptionId.withResourceType("Subscription");
}
subscriptionDao.read(subscriptionId);
return doTriggerSubscription(theResourceIds, theSearchUrls, subscriptionId);
return mySubscriptionTriggeringSvc.triggerSubscription(theResourceIds, theSearchUrls, theSubscriptionId);
}
private IBaseParameters doTriggerSubscription(@OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List<UriParam> theResourceIds, @OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId) {
if (mySubscriptionInterceptorList.isEmpty()) {
throw new PreconditionFailedException("Subscription processing not active on this server");
}
List<UriParam> resourceIds = ObjectUtils.defaultIfNull(theResourceIds, Collections.emptyList());
List<StringParam> searchUrls = ObjectUtils.defaultIfNull(theSearchUrls, Collections.emptyList());
// Make sure we have at least one resource ID or search URL
if (resourceIds.size() == 0 && searchUrls.size() == 0) {
throw new InvalidRequestException("No resource IDs or search URLs specified for triggering");
}
// Resource URLs must be compete
for (UriParam next : resourceIds) {
IdType resourceId = new IdType(next.getValue());
ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasResourceType(), RESOURCE_ID + " parameter must have resource type");
ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasIdPart(), RESOURCE_ID + " parameter must have resource ID part");
}
// Search URLs must be valid
for (StringParam next : searchUrls) {
if (next.getValue().contains("?") == false) {
throw new InvalidRequestException("Search URL is not valid (must be in the form \"[resource type]?[optional params]\")");
}
}
SubscriptionTriggeringJobDetails jobDetails = new SubscriptionTriggeringJobDetails();
jobDetails.setJobId(UUID.randomUUID().toString());
jobDetails.setRemainingResourceIds(resourceIds.stream().map(UriParam::getValue).collect(Collectors.toList()));
jobDetails.setRemainingSearchUrls(searchUrls.stream().map(StringParam::getValue).collect(Collectors.toList()));
if (theSubscriptionId != null) {
jobDetails.setSubscriptionId(theSubscriptionId.toUnqualifiedVersionless().getValue());
}
// Submit job for processing
synchronized (myActiveJobs) {
myActiveJobs.add(jobDetails);
}
ourLog.info("Subscription triggering requested for {} resource and {} search - Gave job ID: {}", resourceIds.size(), searchUrls.size(), jobDetails.getJobId());
// Create a parameters response
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
IPrimitiveType<?> value = (IPrimitiveType<?>) myFhirContext.getElementDefinition("string").newInstance();
value.setValueAsString("Subscription triggering job submitted as JOB ID: " + jobDetails.myJobId);
ParametersUtil.addParameterToParameters(myFhirContext, retVal, "information", value);
return retVal;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return myFhirContext.getResourceDefinition("Subscription").getImplementingClass();
}
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_SECOND)
public void runDeliveryPass() {
synchronized (myActiveJobs) {
if (myActiveJobs.isEmpty()) {
return;
}
String activeJobIds = myActiveJobs.stream().map(t->t.getJobId()).collect(Collectors.joining(", "));
ourLog.info("Starting pass: currently have {} active job IDs: {}", myActiveJobs.size(), activeJobIds);
SubscriptionTriggeringJobDetails activeJob = myActiveJobs.get(0);
runJob(activeJob);
// If the job is complete, remove it from the queue
if (activeJob.getRemainingResourceIds().isEmpty()) {
if (activeJob.getRemainingSearchUrls().isEmpty()) {
if (isBlank(activeJob.myCurrentSearchUuid)) {
myActiveJobs.remove(0);
String remainingJobsMsg = "";
if (myActiveJobs.size() > 0) {
remainingJobsMsg = "(" + myActiveJobs.size() + " jobs remaining)";
}
ourLog.info("Subscription triggering job {} is complete{}", activeJob.getJobId(), remainingJobsMsg);
}
}
}
}
}
private void runJob(SubscriptionTriggeringJobDetails theJobDetails) {
StopWatch sw = new StopWatch();
ourLog.info("Starting pass of subscription triggering job {}", theJobDetails.getJobId());
// Submit individual resources
int totalSubmitted = 0;
List<Pair<String, Future<Void>>> futures = new ArrayList<>();
while (theJobDetails.getRemainingResourceIds().size() > 0 && totalSubmitted < myMaxSubmitPerPass) {
totalSubmitted++;
String nextResourceId = theJobDetails.getRemainingResourceIds().remove(0);
Future<Void> future = submitResource(theJobDetails.getSubscriptionId(), nextResourceId);
futures.add(Pair.of(nextResourceId, future));
}
// Make sure these all succeeded in submitting
if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) {
return;
}
// If we don't have an active search started, and one needs to be.. start it
if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) {
String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0);
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, nextSearchUrl);
String queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?'));
String resourceType = resourceDef.getName();
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceType);
SearchParameterMap params = BaseHapiFhirDao.translateMatchUrl(callingDao, myFhirContext, queryPart, resourceDef);
ourLog.info("Triggering job[{}] is starting a search for {}", theJobDetails.getJobId(), nextSearchUrl);
IBundleProvider search = mySearchCoordinatorSvc.registerSearch(callingDao, params, resourceType, new CacheControlDirective());
theJobDetails.setCurrentSearchUuid(search.getUuid());
theJobDetails.setCurrentSearchResourceType(resourceType);
theJobDetails.setCurrentSearchCount(params.getCount());
theJobDetails.setCurrentSearchLastUploadedIndex(-1);
}
// If we have an active search going, submit resources from it
if (isNotBlank(theJobDetails.getCurrentSearchUuid()) && totalSubmitted < myMaxSubmitPerPass) {
int fromIndex = theJobDetails.getCurrentSearchLastUploadedIndex() + 1;
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(theJobDetails.getCurrentSearchResourceType());
int maxQuerySize = myMaxSubmitPerPass - totalSubmitted;
int toIndex = fromIndex + maxQuerySize;
if (theJobDetails.getCurrentSearchCount() != null) {
toIndex = Math.min(toIndex, theJobDetails.getCurrentSearchCount());
}
ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
List<Long> resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size());
int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex();
for (Long next : resourceIds) {
IBaseResource nextResource = resourceDao.readByPid(next);
Future<Void> future = submitResource(theJobDetails.getSubscriptionId(), nextResource);
futures.add(Pair.of(nextResource.getIdElement().getIdPart(), future));
totalSubmitted++;
highestIndexSubmitted++;
}
if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) {
return;
}
theJobDetails.setCurrentSearchLastUploadedIndex(highestIndexSubmitted);
if (resourceIds.size() == 0 || (theJobDetails.getCurrentSearchCount() != null && toIndex >= theJobDetails.getCurrentSearchCount())) {
ourLog.info("Triggering job[{}] search {} has completed ", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid());
theJobDetails.setCurrentSearchResourceType(null);
theJobDetails.setCurrentSearchUuid(null);
theJobDetails.setCurrentSearchLastUploadedIndex(-1);
theJobDetails.setCurrentSearchCount(null);
}
}
ourLog.info("Subscription trigger job[{}] triggered {} resources in {}ms ({} res / second)", theJobDetails.getJobId(), totalSubmitted, sw.getMillis(), sw.getThroughput(totalSubmitted, TimeUnit.SECONDS));
}
private boolean validateFuturesAndReturnTrueIfWeShouldAbort(List<Pair<String, Future<Void>>> theIdToFutures) {
for (Pair<String, Future<Void>> next : theIdToFutures) {
String nextDeliveredId = next.getKey();
try {
Future<Void> nextFuture = next.getValue();
nextFuture.get();
ourLog.info("Finished redelivering {}", nextDeliveredId);
} catch (Exception e) {
ourLog.error("Failure triggering resource " + nextDeliveredId, e);
return true;
}
}
// Clear the list since it will potentially get reused
theIdToFutures.clear();
return false;
}
private Future<Void> submitResource(String theSubscriptionId, String theResourceIdToTrigger) {
org.hl7.fhir.r4.model.IdType resourceId = new org.hl7.fhir.r4.model.IdType(theResourceIdToTrigger);
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceId.getResourceType());
IBaseResource resourceToTrigger = dao.read(resourceId);
return submitResource(theSubscriptionId, resourceToTrigger);
}
private Future<Void> submitResource(String theSubscriptionId, IBaseResource theResourceToTrigger) {
ourLog.info("Submitting resource {} to subscription {}", theResourceToTrigger.getIdElement().toUnqualifiedVersionless().getValue(), theSubscriptionId);
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theResourceToTrigger.getIdElement());
msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE);
msg.setSubscriptionId(new IdType(theSubscriptionId).toUnqualifiedVersionless().getValue());
msg.setNewPayload(myFhirContext, theResourceToTrigger);
return myExecutorService.submit(()->{
for (int i = 0; ; i++) {
try {
for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) {
next.submitResourceModified(msg);
}
break;
} catch (Exception e) {
if (i >= 3) {
throw new InternalErrorException(e);
}
ourLog.warn("Exception while retriggering subscriptions (going to sleep and retry): {}", e.toString());
Thread.sleep(1000);
}
}
return null;
});
}
public void cancelAll() {
synchronized (myActiveJobs) {
myActiveJobs.clear();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
myAppCtx = applicationContext;
}
private static class SubscriptionTriggeringJobDetails {
private String myJobId;
private String mySubscriptionId;
private List<String> myRemainingResourceIds;
private List<String> myRemainingSearchUrls;
private String myCurrentSearchUuid;
private Integer myCurrentSearchCount;
private String myCurrentSearchResourceType;
private int myCurrentSearchLastUploadedIndex;
public Integer getCurrentSearchCount() {
return myCurrentSearchCount;
}
public void setCurrentSearchCount(Integer theCurrentSearchCount) {
myCurrentSearchCount = theCurrentSearchCount;
}
public String getCurrentSearchResourceType() {
return myCurrentSearchResourceType;
}
public void setCurrentSearchResourceType(String theCurrentSearchResourceType) {
myCurrentSearchResourceType = theCurrentSearchResourceType;
}
public String getJobId() {
return myJobId;
}
public void setJobId(String theJobId) {
myJobId = theJobId;
}
public String getSubscriptionId() {
return mySubscriptionId;
}
public void setSubscriptionId(String theSubscriptionId) {
mySubscriptionId = theSubscriptionId;
}
public List<String> getRemainingResourceIds() {
return myRemainingResourceIds;
}
public void setRemainingResourceIds(List<String> theRemainingResourceIds) {
myRemainingResourceIds = theRemainingResourceIds;
}
public List<String> getRemainingSearchUrls() {
return myRemainingSearchUrls;
}
public void setRemainingSearchUrls(List<String> theRemainingSearchUrls) {
myRemainingSearchUrls = theRemainingSearchUrls;
}
public String getCurrentSearchUuid() {
return myCurrentSearchUuid;
}
public void setCurrentSearchUuid(String theCurrentSearchUuid) {
myCurrentSearchUuid = theCurrentSearchUuid;
}
public int getCurrentSearchLastUploadedIndex() {
return myCurrentSearchLastUploadedIndex;
}
public void setCurrentSearchLastUploadedIndex(int theCurrentSearchLastUploadedIndex) {
myCurrentSearchLastUploadedIndex = theCurrentSearchLastUploadedIndex;
}
}
}

View File

@ -19,21 +19,23 @@ package ca.uhn.fhir.jpa.provider.dstu3;
* limitations under the License.
* #L%
*/
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CapabilityStatement.*;
import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.ExtensionConstants;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CapabilityStatement.*;
import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ -41,11 +43,11 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
private volatile CapabilityStatement myCachedValue;
private DaoConfig myDaoConfig;
private ISearchParamRegistry mySearchParamRegistry;
private String myImplementationDescription;
private boolean myIncludeResourceCounts;
private RestfulServer myRestfulServer;
private IFhirSystemDao<Bundle, Meta> mySystemDao;
/**
* Constructor
*/
@ -65,9 +67,14 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
mySystemDao = theSystemDao;
myDaoConfig = theDaoConfig;
super.setCache(false);
setSearchParamRegistry(theSystemDao.getSearchParamRegistry());
setIncludeResourceCounts(true);
}
public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry;
}
@Override
public CapabilityStatement getServerConformance(HttpServletRequest theRequest) {
CapabilityStatement retVal = myCachedValue;
@ -99,7 +106,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
nextResource.getSearchParam().clear();
String resourceName = nextResource.getType();
RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
Collection<RuntimeSearchParam> searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef);
Collection<RuntimeSearchParam> searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef);
for (RuntimeSearchParam runtimeSp : searchParams) {
CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam();
@ -152,6 +159,10 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
return myIncludeResourceCounts;
}
public void setIncludeResourceCounts(boolean theIncludeResourceCounts) {
myIncludeResourceCounts = theIncludeResourceCounts;
}
/**
* Subclasses may override
*/
@ -168,10 +179,6 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
myImplementationDescription = theImplDesc;
}
public void setIncludeResourceCounts(boolean theIncludeResourceCounts) {
myIncludeResourceCounts = theIncludeResourceCounts;
}
@Override
public void setRestfulServer(RestfulServer theRestfulServer) {
this.myRestfulServer = theRestfulServer;

View File

@ -23,6 +23,7 @@ import java.util.*;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.CapabilityStatement.*;
import org.hl7.fhir.r4.model.Enumerations.SearchParamType;
@ -41,6 +42,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
private volatile CapabilityStatement myCachedValue;
private DaoConfig myDaoConfig;
private ISearchParamRegistry mySearchParamRegistry;
private String myImplementationDescription;
private boolean myIncludeResourceCounts;
private RestfulServer myRestfulServer;
@ -66,6 +68,11 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
myDaoConfig = theDaoConfig;
super.setCache(false);
setIncludeResourceCounts(true);
setSearchParamRegistry(theSystemDao.getSearchParamRegistry());
}
public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry;
}
@Override
@ -99,7 +106,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
nextResource.getSearchParam().clear();
String resourceName = nextResource.getType();
RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
Collection<RuntimeSearchParam> searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef);
Collection<RuntimeSearchParam> searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef);
for (RuntimeSearchParam runtimeSp : searchParams) {
CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam();

View File

@ -26,7 +26,9 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.BasePagingProvider;
import ca.uhn.fhir.rest.server.IPagingProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DatabaseBackedPagingProvider extends BasePagingProvider implements IPagingProvider {
@Autowired

View File

@ -276,8 +276,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
protected List<IBaseResource> toResourceList(ISearchBuilder sb, List<Long> pidsSubList) {
Set<Long> includedPids = new HashSet<>();
if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) {
includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
}
// Execute the query and make sure we return distinct results

View File

@ -47,11 +47,15 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.data.domain.AbstractPageRequest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@ -68,6 +72,7 @@ import java.util.concurrent.*;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@Component("mySearchCoordinatorSvc")
public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public static final int DEFAULT_SYNC_SIZE = 250;
@ -240,8 +245,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
});
} catch (Exception e) {
ourLog.warn("Failed to activate search: {}", e.toString());
// FIXME: aaaaa
ourLog.info("Failed to activate search", e);
ourLog.trace("Failed to activate search", e);
return Optional.empty();
}
}
@ -313,8 +317,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* individually for pages as we return them to clients
*/
final Set<Long> includedPids = new HashSet<>();
includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)"));
List<IBaseResource> resources = new ArrayList<>();
sb.loadResourcesByPid(pids, resources, includedPids, false, myEntityManager, myContext, theCallingDao);
@ -513,10 +517,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* user has requested resources 0-60, then they would get 0-50 back but the search
* coordinator would then stop searching.SearchCoordinatorSvcImplTest
*/
// FIXME: aaaaaaaa
// List<Long> remainingResources = SearchCoordinatorSvcImpl.this.getResources(mySearch.getUuid(), mySyncedPids.size(), theToIndex);
// ourLog.debug("Adding {} resources to the existing {} synced resource IDs", remainingResources.size(), mySyncedPids.size());
// mySyncedPids.addAll(remainingResources);
keepWaiting = false;
break;
case FAILED:

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.dstu3.model.InstantType;
import org.springframework.beans.factory.annotation.Autowired;
@ -36,13 +37,17 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Date;
import java.util.List;
/**
* Deletes old searches
*/
//
// NOTE: This is not a @Service because we manually instantiate
// it in BaseConfig. This is so that we can override the definition
// in Smile.
//
public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
public static final long DEFAULT_CUTOFF_SLACK = 10 * DateUtils.MILLIS_PER_SECOND;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StaleSearchDeletingSvcImpl.class);
@ -51,7 +56,10 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
* DELETE FROM foo WHERE params IN (aaaa)
* type query and this can fail if we have 1000s of params
*/
public static int ourMaximumResultsToDelete = 500;
public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT = 500;
public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS = 20000;
private static int ourMaximumResultsToDeleteInOneStatement = DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT;
private static int ourMaximumResultsToDeleteInOnePass = DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS;
private static Long ourNowForUnitTests;
/*
* We give a bit of extra leeway just to avoid race conditions where a query result
@ -69,8 +77,6 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
private ISearchResultDao mySearchResultDao;
@Autowired
private PlatformTransactionManager myTransactionManager;
@PersistenceContext()
private EntityManager myEntityManager;
private void deleteSearch(final Long theSearchPid) {
mySearchDao.findById(theSearchPid).ifPresent(searchToDelete -> {
@ -84,10 +90,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
* huge deal to be only partially deleting search results. They'll get deleted
* eventually
*/
int max = ourMaximumResultsToDelete;
int max = ourMaximumResultsToDeleteInOnePass;
Slice<Long> resultPids = mySearchResultDao.findForSearch(PageRequest.of(0, max), searchToDelete.getId());
if (resultPids.hasContent()) {
mySearchResultDao.deleteByIds(resultPids.getContent());
List<List<Long>> partitions = Lists.partition(resultPids.getContent(), ourMaximumResultsToDeleteInOneStatement);
for (List<Long> nextPartition : partitions) {
mySearchResultDao.deleteByIds(nextPartition);
}
}
// Only delete if we don't have results left in this search
@ -158,9 +168,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
myCutoffSlack = theCutoffSlack;
}
@VisibleForTesting
public static void setMaximumResultsToDeleteInOnePassForUnitTest(int theMaximumResultsToDeleteInOnePass) {
ourMaximumResultsToDeleteInOnePass = theMaximumResultsToDeleteInOnePass;
}
@VisibleForTesting
public static void setMaximumResultsToDeleteForUnitTest(int theMaximumResultsToDelete) {
ourMaximumResultsToDelete = theMaximumResultsToDelete;
ourMaximumResultsToDeleteInOneStatement = theMaximumResultsToDelete;
}
private static long now() {

View File

@ -24,17 +24,24 @@ public interface IResourceReindexingSvc {
/**
* Marks all indexes as needing fresh indexing
*
* @return Returns the job ID
*/
void markAllResourcesForReindexing();
Long markAllResourcesForReindexing();
/**
* Marks all indexes of the given type as needing fresh indexing
*
* @return Returns the job ID
*/
void markAllResourcesForReindexing(String theType);
Long markAllResourcesForReindexing(String theType);
/**
* Called automatically by the job scheduler
*
*/
void scheduleReindexingPass();
/**
* @return Returns null if the system did not attempt to perform a pass because one was
* already proceeding. Otherwise, returns the number of resources affected.
*/

View File

@ -73,6 +73,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
private static final Date BEGINNING_OF_TIME = new Date(0);
private static final Logger ourLog = LoggerFactory.getLogger(ResourceReindexingSvcImpl.class);
private static final int PASS_SIZE = 25000;
private final ReentrantLock myIndexingLock = new ReentrantLock();
@Autowired
private IResourceReindexJobDao myReindexJobDao;
@ -149,13 +150,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
@Override
@Transactional(Transactional.TxType.REQUIRED)
public void markAllResourcesForReindexing() {
markAllResourcesForReindexing(null);
public Long markAllResourcesForReindexing() {
return markAllResourcesForReindexing(null);
}
@Override
@Transactional(Transactional.TxType.REQUIRED)
public void markAllResourcesForReindexing(String theType) {
public Long markAllResourcesForReindexing(String theType) {
String typeDesc;
if (isNotBlank(theType)) {
myReindexJobDao.markAllOfTypeAsDeleted(theType);
@ -171,11 +172,19 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
job = myReindexJobDao.saveAndFlush(job);
ourLog.info("Marking all resources of type {} for reindexing - Got job ID[{}]", typeDesc, job.getId());
return job.getId();
}
@Override
@Transactional(Transactional.TxType.NEVER)
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
public void scheduleReindexingPass() {
runReindexingPass();
}
@Override
@Transactional(Transactional.TxType.NEVER)
public Integer runReindexingPass() {
if (myDaoConfig.isSchedulingDisabled()) {
return null;
@ -223,10 +232,16 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
Collection<ResourceReindexJobEntity> jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false));
assert jobs != null;
if (jobs.size() > 0) {
ourLog.info("Running {} reindex jobs: {}", jobs.size(), jobs);
} else {
ourLog.debug("Running {} reindex jobs: {}", jobs.size(), jobs);
}
int count = 0;
for (ResourceReindexJobEntity next : jobs) {
if (next.getThresholdHigh().getTime() < System.currentTimeMillis()) {
if (next.getThresholdLow() != null && next.getThresholdLow().getTime() >= next.getThresholdHigh().getTime()) {
markJobAsDeleted(next);
continue;
}
@ -236,9 +251,10 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
return count;
}
private void markJobAsDeleted(ResourceReindexJobEntity next) {
private void markJobAsDeleted(ResourceReindexJobEntity theJob) {
ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId());
myTxTemplate.execute(t -> {
myReindexJobDao.markAsDeletedById(next.getId());
myReindexJobDao.markAsDeletedById(theJob.getId());
return null;
});
}
@ -259,8 +275,9 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
Date high = theJob.getThresholdHigh();
// Query for resources within threshold
StopWatch pageSw = new StopWatch();
Slice<Long> range = myTxTemplate.execute(t -> {
PageRequest page = PageRequest.of(0, 10000);
PageRequest page = PageRequest.of(0, PASS_SIZE);
if (isNotBlank(theJob.getResourceType())) {
return myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(page, theJob.getResourceType(), low, high);
} else {
@ -269,6 +286,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
});
Validate.notNull(range);
int count = range.getNumberOfElements();
ourLog.info("Loaded {} resources for reindexing in {}", count, pageSw.toString());
// If we didn't find any results at all, mark as deleted
if (count == 0) {
markJobAsDeleted(theJob);
return 0;
}
// Submit each resource requiring reindexing
List<Future<Date>> futures = range
@ -277,7 +301,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
.collect(Collectors.toList());
Date latestDate = null;
boolean haveMultipleDates = false;
for (Future<Date> next : futures) {
Date nextDate;
try {
@ -293,29 +316,22 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
}
if (nextDate != null) {
if (latestDate != null) {
if (latestDate.getTime() != nextDate.getTime()) {
haveMultipleDates = true;
}
}
if (latestDate == null || latestDate.getTime() < nextDate.getTime()) {
latestDate = new Date(nextDate.getTime());
}
}
}
Validate.notNull(latestDate);
Date newLow;
if (latestDate.getTime() == low.getTime()) {
if (count == PASS_SIZE) {
// Just in case we end up in some sort of infinite loop. This shouldn't happen, and couldn't really
// happen unless there were 10000 resources with the exact same update time down to the
// millisecond.
Date newLow;
if (latestDate == null) {
markJobAsDeleted(theJob);
return 0;
}
if (latestDate.getTime() == low.getTime()) {
ourLog.error("Final pass time for reindex JOB[{}] has same ending low value: {}", theJob.getId(), latestDate);
newLow = new Date(latestDate.getTime() + 1);
} else if (!haveMultipleDates) {
}
newLow = new Date(latestDate.getTime() + 1);
} else {
newLow = latestDate;
@ -323,10 +339,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
myTxTemplate.execute(t -> {
myReindexJobDao.setThresholdLow(theJob.getId(), newLow);
Integer existingCount = myReindexJobDao.getReindexCount(theJob.getId()).orElse(0);
int newCount = existingCount + counter.get();
myReindexJobDao.setReindexCount(theJob.getId(), newCount);
return null;
});
ourLog.info("Completed pass of reindex JOB[{}] - Indexed {} resources in {} ({} / sec) - Have indexed until: {}", theJob.getId(), count, sw.toString(), sw.formatThroughput(count, TimeUnit.SECONDS), theJob.getThresholdLow());
ourLog.info("Completed pass of reindex JOB[{}] - Indexed {} resources in {} ({} / sec) - Have indexed until: {}", theJob.getId(), count, sw.toString(), sw.formatThroughput(count, TimeUnit.SECONDS), newLow);
return counter.get();
}
@ -450,6 +469,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
return e;
}
});
} catch (ResourceVersionConflictException e) {
/*
* We reindex in multiple threads, so it's technically possible that two threads try
@ -458,7 +478,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
* not get this error, so we'll let the other one fail and try
* again later.
*/
ourLog.info("Failed to reindex {} because of a version conflict. Leaving in unindexed state: {}", e.getMessage());
ourLog.info("Failed to reindex because of a version conflict. Leaving in unindexed state: {}", e.getMessage());
reindexFailure = null;
}

View File

@ -23,10 +23,15 @@ package ca.uhn.fhir.jpa.search.warm;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.parser.DataFormatException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
@ -34,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
@Autowired
@ -43,6 +49,8 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private FhirContext myCtx;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Override
@Scheduled(fixedDelay = 1000)
@ -72,7 +80,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl);
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName());
String queryPart = parseWarmUrlParamPart(nextUrl);
SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(callingDao, myCtx, queryPart, resourceDef);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef);
callingDao.search(responseCriteriaUrl);
}

View File

@ -25,10 +25,12 @@ import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.Map.Entry;
@Service
public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
@Autowired

View File

@ -20,21 +20,19 @@ package ca.uhn.fhir.jpa.subscription;
* #L%
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;
public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptionSubscriber {
private static final Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionDeliverySubscriber.class);
public BaseSubscriptionDeliverySubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor);
public BaseSubscriptionDeliverySubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theChannelType, theSubscriptionInterceptor);
}
@Override
@ -59,21 +57,6 @@ public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptio
return;
}
// Load the resource
IIdType payloadId = msg.getPayloadId(getContext());
Class type = getContext().getResourceDefinition(payloadId.getResourceType()).getImplementingClass();
IFhirResourceDao dao = getSubscriptionInterceptor().getDao(type);
IBaseResource loadedPayload;
try {
loadedPayload = dao.read(payloadId);
} catch (ResourceNotFoundException e) {
// This can happen if a last minute failure happens when saving a resource,
// eg a constraint causes the transaction to roll back on commit
ourLog.warn("Unable to find resource {} - Aborting delivery", payloadId.getValue());
return;
}
msg.setPayload(getContext(), loadedPayload);
handleMessage(msg);
} catch (Exception e) {
String msg = "Failure handling subscription payload for subscription: " + subscriptionId;

View File

@ -1,42 +1,27 @@
package ca.uhn.fhir.jpa.subscription;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherCompositeInMemoryDatabase;
import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherDatabase;
import ca.uhn.fhir.jpa.util.JpaConstants;
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.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
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 ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
@ -52,6 +37,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -73,11 +59,32 @@ import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 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 abstract class BaseSubscriptionInterceptor<S extends IBaseResource> extends ServerOperationInterceptorAdapter {
static final String SUBSCRIPTION_STATUS = "Subscription.status";
static final String SUBSCRIPTION_TYPE = "Subscription.channel.type";
private static final Integer MAX_SUBSCRIPTION_RESULTS = 1000;
private static boolean ourForcePayloadEncodeAndDecodeForUnitTests;
private final Object myInitSubscriptionsLock = new Object();
private SubscribableChannel myProcessingChannel;
private Map<String, SubscribableChannel> myDeliveryChannel;
@ -91,9 +98,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class);
private ThreadPoolExecutor myDeliveryExecutor;
private LinkedBlockingQueue<Runnable> myProcessingExecutorQueue;
private IFhirResourceDao<?> mySubscriptionDao;
@Autowired
private List<IFhirResourceDao<?>> myResourceDaos;
@Autowired
private FhirContext myCtx;
@Autowired(required = false)
@ -104,7 +108,16 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
@Autowired
@Qualifier(BaseConfig.TASK_EXECUTOR_NAME)
private AsyncTaskExecutor myAsyncTaskExecutor;
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
@Autowired
private SubscriptionMatcherCompositeInMemoryDatabase mySubscriptionMatcherCompositeInMemoryDatabase;
@Autowired
private SubscriptionMatcherDatabase mySubscriptionMatcherDatabase;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private BeanFactory beanFactory;
@Autowired
private MatchUrlService myMatchUrlService;
private Semaphore myInitSubscriptionsSemaphore = new Semaphore(1);
/**
@ -286,26 +299,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
public abstract Subscription.SubscriptionChannelType getChannelType();
// TODO KHS move out
@SuppressWarnings("unchecked")
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
if (myResourceTypeToDao == null) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> theResourceTypeToDao = new HashMap<>();
for (IFhirResourceDao<?> next : myResourceDaos) {
theResourceTypeToDao.put(next.getResourceType(), next);
}
if (this instanceof IFhirResourceDao<?>) {
IFhirResourceDao<?> thiz = (IFhirResourceDao<?>) this;
theResourceTypeToDao.put(thiz.getResourceType(), thiz);
}
myResourceTypeToDao = theResourceTypeToDao;
}
return (IFhirResourceDao<R>) myResourceTypeToDao.get(theType);
}
protected MessageChannel getDeliveryChannel(CanonicalSubscription theSubscription) {
return mySubscribableChannel.get(theSubscription.getIdElement(myCtx).getIdPart());
}
@ -335,10 +328,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
myProcessingChannel = theProcessingChannel;
}
protected IFhirResourceDao<?> getSubscriptionDao() {
return mySubscriptionDao;
}
public List<CanonicalSubscription> getRegisteredSubscriptions() {
return new ArrayList<>(myIdToSubscription.values());
}
@ -378,7 +367,8 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
IBundleProvider subscriptionBundleList = getSubscriptionDao().search(map, req);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
IBundleProvider subscriptionBundleList = subscriptionDao.search(map, req);
if (subscriptionBundleList.size() >= MAX_SUBSCRIPTION_RESULTS) {
ourLog.error("Currently over " + MAX_SUBSCRIPTION_RESULTS + " subscriptions. Some subscriptions have not been loaded.");
}
@ -436,19 +426,14 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
protected void registerSubscriptionCheckingSubscriber() {
if (mySubscriptionCheckingSubscriber == null) {
ISubscriptionMatcher subscriptionMatcher = new SubscriptionMatcherDatabase(getSubscriptionDao(), this);
mySubscriptionCheckingSubscriber = new SubscriptionCheckingSubscriber(getSubscriptionDao(), getChannelType(), this, subscriptionMatcher );
mySubscriptionCheckingSubscriber = beanFactory.getBean(SubscriptionCheckingSubscriber.class, getChannelType(), this, mySubscriptionMatcherCompositeInMemoryDatabase);
}
getProcessingChannel().subscribe(mySubscriptionCheckingSubscriber);
}
@Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theResource.getIdElement());
msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.CREATE);
msg.setNewPayload(myCtx, theResource);
submitResourceModified(msg);
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
}
@Override
@ -465,10 +450,17 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
}
void submitResourceModifiedForUpdate(IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theNewResource.getIdElement());
msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE);
msg.setOperationType(theOperationType);
msg.setNewPayload(myCtx, theNewResource);
if (ourForcePayloadEncodeAndDecodeForUnitTests) {
msg.clearPayloadDecoded();
}
submitResourceModified(msg);
}
@ -501,10 +493,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
myCtx = theCtx;
}
public void setResourceDaos(List<IFhirResourceDao<?>> theResourceDaos) {
myResourceDaos = theResourceDaos;
}
@VisibleForTesting
public void setTxManager(PlatformTransactionManager theTxManager) {
myTxManager = theTxManager;
@ -512,15 +500,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
@PostConstruct
public void start() {
for (IFhirResourceDao<?> next : myResourceDaos) {
if (next.getResourceType() != null) {
if (myCtx.getResourceDefinition(next.getResourceType()).getName().equals("Subscription")) {
mySubscriptionDao = next;
}
}
}
Validate.notNull(mySubscriptionDao);
if (myCtx.getVersion().getVersion() == FhirVersionEnum.R4) {
Validate.notNull(myEventDefinitionDaoR4);
}
@ -555,7 +534,8 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
}
if (mySubscriptionActivatingSubscriber == null) {
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager, myAsyncTaskExecutor);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(subscriptionDao, getChannelType(), this, myTxManager, myAsyncTaskExecutor);
}
registerSubscriptionCheckingSubscriber();
@ -619,5 +599,31 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
return myIdToSubscription.remove(subscriptionId);
}
public IFhirResourceDao<?> getSubscriptionDao() {
return myDaoRegistry.getResourceDao("Subscription");
}
public IFhirResourceDao getDao(Class type) {
return myDaoRegistry.getResourceDao(type);
}
public void setResourceDaos(List<IFhirResourceDao> theResourceDaos) {
myDaoRegistry.setResourceDaos(theResourceDaos);
}
public void validateCriteria(final S theResource) {
CanonicalSubscription subscription = canonicalize(theResource);
String criteria = subscription.getCriteriaString();
try {
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myCtx, criteria);
myMatchUrlService.translateMatchUrl(criteria, resourceDef);
} catch (InvalidRequestException e) {
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage());
}
}
@VisibleForTesting
public static void setForcePayloadEncodeAndDecodeForUnitTests(boolean theForcePayloadEncodeAndDecodeForUnitTests) {
ourForcePayloadEncodeAndDecodeForUnitTests = theForcePayloadEncodeAndDecodeForUnitTests;
}
}

View File

@ -21,25 +21,40 @@ package ca.uhn.fhir.jpa.subscription;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHandler;
import javax.annotation.PostConstruct;
public abstract class BaseSubscriptionSubscriber implements MessageHandler {
private final IFhirResourceDao<?> mySubscriptionDao;
private final Subscription.SubscriptionChannelType myChannelType;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
@Autowired
DaoRegistry myDaoRegistry;
private IFhirResourceDao<?> mySubscriptionDao;
/**
* Constructor
*/
public BaseSubscriptionSubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
mySubscriptionDao = theSubscriptionDao;
public BaseSubscriptionSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
myChannelType = theChannelType;
mySubscriptionInterceptor = theSubscriptionInterceptor;
}
@SuppressWarnings("unused") // Don't delete, used in Smile
public void setDaoRegistry(DaoRegistry theDaoRegistry) {
myDaoRegistry = theDaoRegistry;
}
@PostConstruct
public void setSubscriptionDao() {
mySubscriptionDao = myDaoRegistry.getResourceDao("Subscription");
}
public Subscription.SubscriptionChannelType getChannelType() {
return myChannelType;
}

Some files were not shown because too many files have changed in this diff Show More