Merging master into working branch.

This commit is contained in:
Diederik Muylwyk 2019-09-26 10:41:34 -04:00
commit a4f9894104
36 changed files with 547 additions and 626 deletions

View File

@ -3,10 +3,10 @@ HAPI FHIR
HAPI FHIR - Java API for HL7 FHIR Clients and Servers
[![Coverage Status](https://coveralls.io/repos/jamesagnew/hapi-fhir/badge.svg?branch=master&service=github)](https://coveralls.io/github/jamesagnew/hapi-fhir?branch=master)
[![Build Status](https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg)](https://codecov.io/gh/jamesagnew/hapi-fhir)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg)](http://search.maven.org/#search|ga|1|ca.uhn.hapi.fhir)
[![License](https://img.shields.io/badge/license-apache%202.0-60C060.svg)](http://jamesagnew.github.io/hapi-fhir/license.html)
[![Build Status](https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master)
Complete project documentation is available here:
http://hapifhir.io

View File

@ -28,7 +28,7 @@ jobs:
inputs:
#mavenPomFile: 'pom.xml'
goals: 'clean install' # Optional
options: ''
options: '-P ALLMODULES,JACOCO'
#publishJUnitResults: true
#testResultsFiles: '**/surefire-reports/TEST-*.xml' # Required when publishJUnitResults == True
#testRunTitle: # Optional
@ -44,7 +44,7 @@ jobs:
#mavenVersionOption: 'Default' # Options: default, path
#mavenDirectory: # Required when mavenVersionOption == Path
#mavenSetM2Home: false # Required when mavenVersionOption == Path
mavenOptions: '-Xmx2048m $(MAVEN_OPTS)' # Optional
mavenOptions: '-Xmx2048m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS' # Optional
#mavenAuthenticateFeed: false
#effectivePomSkip: false
#sonarQubeRunAnalysis: false
@ -53,4 +53,7 @@ jobs:
#pmdRunAnalysis: false # Optional
#findBugsRunAnalysis: false # Optional
- script: bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN)
displayName: 'codecov'

View File

@ -129,12 +129,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>
@ -153,20 +147,6 @@
<argLine>@{argLine} -Dfile.encoding=UTF-8 -Xmx712m</argLine>
</configuration>
</plugin>
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
-->
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>

View File

@ -27,7 +27,6 @@ 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;
@ -68,47 +67,6 @@ public abstract class BaseParser implements IParser {
private boolean mySuppressNarratives;
private Set<String> myDontStripVersionsFromReferencesAtPaths;
private Map<Key, List<CompositeChildElement>> compositeChildrenCache = new HashMap<>();
private static class Key {
private final BaseRuntimeElementCompositeDefinition<?> resDef;
private final boolean theContainedResource;
private final CompositeChildElement theParent;
private final EncodeContext theEncodeContext;
public Key(BaseRuntimeElementCompositeDefinition<?> resDef, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) {
this.resDef = resDef;
this.theContainedResource = theContainedResource;
this.theParent = theParent;
this.theEncodeContext = theEncodeContext;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((resDef == null) ? 0 : resDef.hashCode());
result = prime * result + (theContainedResource ? 1231 : 1237);
result = prime * result + ((theParent == null) ? 0 : theParent.hashCode());
result = prime * result + ((theEncodeContext == null) ? 0 : theEncodeContext.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Key) {
final Key that = (Key) obj;
return Objects.equals(this.resDef, that.resDef) &&
this.theContainedResource == that.theContainedResource &&
Objects.equals(this.theParent, that.theParent) &&
Objects.equals(this.theEncodeContext, that.theEncodeContext);
}
return false;
}
}
/**
* Constructor
*/
@ -171,12 +129,12 @@ public abstract class BaseParser implements IParser {
protected Iterable<CompositeChildElement> compositeChildIterator(IBase theCompositeElement, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) {
BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theCompositeElement.getClass());
return compositeChildrenCache.computeIfAbsent(new Key(elementDef, theContainedResource, theParent, theEncodeContext), (k) -> {
return theEncodeContext.getCompositeChildrenCache().computeIfAbsent(new Key(elementDef, theContainedResource, theParent, theEncodeContext), (k) -> {
final List<BaseRuntimeChildDefinition> children = elementDef.getChildrenAndExtension();
final List<CompositeChildElement> result = new ArrayList<>(children.size());
for(final BaseRuntimeChildDefinition child: children) {
for (final BaseRuntimeChildDefinition child : children) {
CompositeChildElement myNext = new CompositeChildElement(theParent, child, theEncodeContext);
/*
@ -274,7 +232,7 @@ public abstract class BaseParser implements IParser {
}
protected List<IBaseReference> getAllBaseReferences(IBaseResource theResource){
protected List<IBaseReference> getAllBaseReferences(IBaseResource theResource) {
final ArrayList<IBaseReference> retVal = new ArrayList<IBaseReference>();
findBaseReferences(retVal, theResource, myContext.getResourceDefinition(theResource));
return retVal;
@ -288,7 +246,7 @@ public abstract class BaseParser implements IParser {
if (theElement instanceof IBaseReference) {
allElements.add((IBaseReference) theElement);
}
BaseRuntimeElementDefinition<?> def = theDefinition;
if (def.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) {
def = myContext.getElementDefinition(theElement.getClass());
@ -353,7 +311,7 @@ public abstract class BaseParser implements IParser {
}
}
}
private String determineReferenceText(IBaseReference theRef, CompositeChildElement theCompositeChildElement) {
IIdType ref = theRef.getReferenceElement();
if (isBlank(ref.getIdPart())) {
@ -1287,10 +1245,10 @@ public abstract class BaseParser implements IParser {
if (obj instanceof CompositeChildElement) {
final CompositeChildElement that = (CompositeChildElement) obj;
return Objects.equals(this.getEnclosingInstance(), that.getEnclosingInstance()) &&
Objects.equals(this.myDef, that.myDef) &&
Objects.equals(this.myParent, that.myParent) &&
Objects.equals(this.myResDef, that.myResDef) &&
Objects.equals(this.myEncodeContext, that.myEncodeContext);
Objects.equals(this.myDef, that.myDef) &&
Objects.equals(this.myParent, that.myParent) &&
Objects.equals(this.myResDef, that.myResDef) &&
Objects.equals(this.myEncodeContext, that.myEncodeContext);
}
return false;
}
@ -1369,13 +1327,17 @@ public abstract class BaseParser implements IParser {
}
}
/**
* EncodeContext is a shared state object that is passed around the
* encode process
*/
protected class EncodeContext extends EncodeContextPath {
private final ArrayList<EncodeContextPathElement> myResourcePath = new ArrayList<>(10);
private final Map<Key, List<CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
public Map<Key, List<CompositeChildElement>> getCompositeChildrenCache() {
return myCompositeChildrenCache;
}
protected ArrayList<EncodeContextPathElement> getResourcePath() {
return myResourcePath;
@ -1508,6 +1470,46 @@ public abstract class BaseParser implements IParser {
}
}
private static class Key {
private final BaseRuntimeElementCompositeDefinition<?> resDef;
private final boolean theContainedResource;
private final CompositeChildElement theParent;
private final EncodeContext theEncodeContext;
public Key(BaseRuntimeElementCompositeDefinition<?> resDef, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) {
this.resDef = resDef;
this.theContainedResource = theContainedResource;
this.theParent = theParent;
this.theEncodeContext = theEncodeContext;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((resDef == null) ? 0 : resDef.hashCode());
result = prime * result + (theContainedResource ? 1231 : 1237);
result = prime * result + ((theParent == null) ? 0 : theParent.hashCode());
result = prime * result + ((theEncodeContext == null) ? 0 : theEncodeContext.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Key) {
final Key that = (Key) obj;
return Objects.equals(this.resDef, that.resDef) &&
this.theContainedResource == that.theContainedResource &&
Objects.equals(this.theParent, that.theParent) &&
Objects.equals(this.theEncodeContext, that.theEncodeContext);
}
return false;
}
}
static class ContainedResources {
private long myNextContainedId = 1;

View File

@ -104,14 +104,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -43,14 +43,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -51,6 +51,11 @@
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
@ -91,381 +96,29 @@
<artifactId>hapi-fhir-jpaserver-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<exclusions>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.helger</groupId>
<artifactId>ph-schematron</artifactId>
<exclusions>
<exclusion>
<artifactId>Saxon-HE</artifactId>
<groupId>net.sf.saxon</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.helger</groupId>
<artifactId>ph-commons</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
</dependency>
<!-- Use an older version of SLF4j just to make sure we compile correctly against old SLF4j - Some people can't upgrade and we have no real need for recent features. -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<!-- Test Database -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<scope>test</scope>
</dependency>
<!--
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyshared</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbytools</artifactId>
<scope>test</scope>
</dependency>
-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<classifier>jdk15</classifier>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
<exclusion>
<artifactId>commons-lang</artifactId>
<groupId>commons-lang</groupId>
</exclusion>
<exclusion>
<artifactId>ezmorph</artifactId>
<groupId>net.sf.ezmorph</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<classifier>jdk15-sources</classifier>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
<exclusion>
<artifactId>commons-lang</artifactId>
<groupId>commons-lang</groupId>
</exclusion>
<exclusion>
<artifactId>ezmorph</artifactId>
<groupId>net.sf.ezmorph</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>directory-naming</groupId>
<artifactId>naming-java</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!--
For some reason JavaDoc crashed during site generation unless we have this dependency
-->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<skipDeploy>true</skipDeploy>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.basepom.maven</groupId>
<artifactId>duplicate-finder-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>jacoco-merge</id>
<goals>
<goal>merge</goal>
</goals>
<phase>install</phase>
<configuration>
<fileSets>
<fileSet implementation="org.apache.maven.shared.model.fileset.FileSet">
<directory>${basedir}/..</directory>
<includes>
<include>hapi-fhir-base/target/jacoco.exec</include>
<include>hapi-fhir-client/target/jacoco.exec</include>
<include>hapi-fhir-server/target/jacoco.exec</include>
<include>hapi-fhir-structures-dstu/target/jacoco.exec</include>
<include>hapi-fhir-structures-dstu2/target/jacoco.exec</include>
<include>hapi-fhir-structures-hl7org-dstu2/target/jacoco.exec</include>
<include>hapi-fhir-structures-dstu3/target/jacoco.exec</include>
<include>hapi-fhir-structures-r4/target/jacoco.exec</include>
<include>hapi-fhir-jpaserver-model/target/jacoco.exec</include>
<include>hapi-fhir-jpaserver-searchparam/target/jacoco.exec</include>
<include>hapi-fhir-jpaserver-subscription/target/jacoco.exec</include>
<include>hapi-fhir-jpaserver-base/target/jacoco.exec</include>
<include>hapi-fhir-client-okhttp/target/jacoco.exec</include>
<include>hapi-fhir-android/target/jacoco.exec</include>
<include>hapi-fhir-validation/target/jacoco.exec</include>
</includes>
</fileSet>
</fileSets>
</configuration>
</execution>
<execution>
<id>post-integration-test</id>
<phase>install</phase>
<phase>verify</phase>
<goals>
<goal>report</goal>
<goal>report-aggregate</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-report</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eluder.coveralls</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<configuration>
<sourceEncoding>UTF-8</sourceEncoding>
<serviceName>travis-ci</serviceName>
<serviceJobId>${env.TRAVIS_JOB_ID}</serviceJobId>
<sourceDirectories>
<sourceDirectory>../hapi-fhir-base/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-client/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-server/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-structures-dstu/src/test/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-structures-dstu2/src/test/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-structures-hl7org-dstu2/src/test/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-structures-dstu3/src/test/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-structures-r4/src/test/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-jpaserver-model/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-jpaserver-searchparam/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-jpaserver-subscription/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-jpaserver-base/src/main/java</sourceDirectory>
<sourceDirectory>../hapi-fhir-client-okhttp/src/main/java</sourceDirectory>
</sourceDirectories>
</configuration>
<dependencies>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>../hapi-fhir-base/src/main/java</source>
<source>../hapi-fhir-client/src/main/java</source>
<source>../hapi-fhir-server/src/main/java</source>
<source>../hapi-fhir-jpaserver-model/src/main/java</source>
<source>../hapi-fhir-jpaserver-searchparam/src/main/java</source>
<source>../hapi-fhir-jpaserver-subscription/src/main/java</source>
<source>../hapi-fhir-jpaserver-base/src/main/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
<resources>
</resources>
<testResources>
<testResource>
<directory>../hapi-fhir-base/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-jpaserver-model/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-jpaserver-searchparam/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-jpaserver-subscription/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-jpaserver-base/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-structures-dstu/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-structures-dstu2/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-structures-hl7org-dstu2/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-structures-dstu3/src/test/resources</directory>
</testResource>
<testResource>
<directory>../hapi-fhir-client-okhttp/src/test/resources</directory>
</testResource>
</testResources>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</reporting>
</project>

View File

@ -660,14 +660,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@ -44,4 +45,7 @@ public interface ISearchResultDao extends JpaRepository<SearchResult, Long> {
@Modifying
@Query("DELETE FROM SearchResult s WHERE s.myId IN :ids")
void deleteByIds(@Param("ids") List<Long> theContent);
@Query("SELECT count(r) FROM SearchResult r WHERE r.mySearchPid = :search")
int countForSearch(@Param("search") Long theSearchPid);
}

View File

@ -6,7 +6,10 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hibernate.annotations.OptimisticLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@ -49,6 +52,22 @@ public class Search implements ICachedSearchDetails, Serializable {
private static final int MAX_SEARCH_QUERY_STRING = 10000;
private static final int FAILURE_MESSAGE_LENGTH = 500;
private static final long serialVersionUID = 1L;
private static final Logger ourLog = LoggerFactory.getLogger(Search.class);
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myLastUpdatedHigh", myLastUpdatedHigh)
.append("myLastUpdatedLow", myLastUpdatedLow)
.append("myNumFound", myNumFound)
.append("myNumBlocked", myNumBlocked)
.append("myStatus", myStatus)
.append("myTotalCount", myTotalCount)
.append("myUuid", myUuid)
.append("myVersion", myVersion)
.toString();
}
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED", nullable = false, updatable = false)
private Date myCreated;
@ -77,6 +96,8 @@ public class Search implements ICachedSearchDetails, Serializable {
private Date myLastUpdatedLow;
@Column(name = "NUM_FOUND", nullable = false)
private int myNumFound;
@Column(name = "NUM_BLOCKED", nullable = true)
private Integer myNumBlocked;
@Column(name = "PREFERRED_PAGE_SIZE", nullable = true)
private Integer myPreferredPageSize;
@Column(name = "RESOURCE_ID", nullable = true)
@ -118,6 +139,14 @@ public class Search implements ICachedSearchDetails, Serializable {
super();
}
public int getNumBlocked() {
return myNumBlocked != null ? myNumBlocked : 0;
}
public void setNumBlocked(int theNumBlocked) {
myNumBlocked = theNumBlocked;
}
public Date getExpiryOrNull() {
return myExpiryOrNull;
}
@ -196,10 +225,12 @@ public class Search implements ICachedSearchDetails, Serializable {
}
public int getNumFound() {
ourLog.trace("getNumFound {}", myNumFound);
return myNumFound;
}
public void setNumFound(int theNumFound) {
ourLog.trace("setNumFound {}", theNumFound);
myNumFound = theNumFound;
}
@ -260,10 +291,12 @@ public class Search implements ICachedSearchDetails, Serializable {
}
public SearchStatusEnum getStatus() {
ourLog.trace("getStatus {}", myStatus);
return myStatus;
}
public void setStatus(SearchStatusEnum theStatus) {
ourLog.trace("setStatus {}", theStatus);
myStatus = theStatus;
}

View File

@ -53,7 +53,7 @@ public class TermCodeSystemVersion implements Serializable {
@JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_CODESYSVER_RES_ID"))
private ResourceTable myResource;
@Column(name = "RES_ID", insertable = false, updatable = false)
@Column(name = "RES_ID", nullable = false, insertable = false, updatable = false)
private Long myResourcePid;
@Column(name = "CS_VERSION_ID", nullable = true, updatable = false, length = MAX_VERSION_LENGTH)

View File

@ -232,7 +232,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
return mySearchResultCacheSvc.fetchResultPids(search, theFrom, theTo);
List<Long> pids = mySearchResultCacheSvc.fetchResultPids(search, theFrom, theTo);
return pids;
}
@ -652,6 +656,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
ArrayList<Long> unsyncedPids = myUnsyncedPids;
int countBlocked = 0;
// Interceptor call: STORAGE_PREACCESS_RESOURCES
// This can be used to remove results from the search result details before
@ -669,6 +674,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
unsyncedPids.remove(i);
myCountBlockedThisPass++;
myCountSavedTotal++;
countBlocked++;
}
}
}
@ -685,7 +691,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
unsyncedPids.clear();
if (theResultIter.hasNext() == false) {
mySearch.setNumFound(myCountSavedTotal);
int skippedCount = theResultIter.getSkippedCount();
int totalFetched = skippedCount + myCountSavedThisPass + myCountBlockedThisPass;
ourLog.trace("MaxToFetch[{}] SkippedCount[{}] CountSavedThisPass[{}] CountSavedThisTotal[{}] AdditionalPrefetchRemaining[{}]", myMaxResultsToFetch, skippedCount, myCountSavedThisPass, myCountSavedTotal, myAdditionalPrefetchThresholdsRemaining);
@ -707,6 +712,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
mySearch.setNumFound(myCountSavedTotal);
mySearch.setNumBlocked(mySearch.getNumBlocked() + countBlocked);
int numSynced;
synchronized (mySyncedPids) {
@ -1033,10 +1039,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
return super.call();
}
@Override
public List<Long> getResourcePids(int theFromIndex, int theToIndex) {
return super.getResourcePids(theFromIndex, theToIndex);
}
}
/**

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -55,7 +56,10 @@ public class DatabaseSearchResultCacheSvcImpl implements ISearchResultCacheSvc {
.findWithSearchPid(theSearch.getId(), page)
.getContent();
ourLog.trace("fetchResultPids for range {}-{} returned {} pids", theFrom, theTo, retVal.size());
ourLog.debug("fetchResultPids for range {}-{} returned {} pids", theFrom, theTo, retVal.size());
// FIXME: should we remove the blocked number from this message?
Validate.isTrue((theSearch.getNumFound() - theSearch.getNumBlocked()) < theTo || retVal.size() == (theTo - theFrom), "Failed to find results in cache, requested %d - %d and got %d with total found=%d and blocked %s", theFrom, theTo, retVal.size(), theSearch.getNumFound(), theSearch.getNumBlocked());
return new ArrayList<>(retVal);
}

View File

@ -77,6 +77,10 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
private static JpaValidationSupportChainR4 ourJpaValidationSupportChainR4;
private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao;
@Autowired
protected ISearchDao mySearchEntityDao;
@Autowired
protected ISearchResultDao mySearchResultDao;
@Autowired
@Qualifier("myResourceCountsCache")
protected ResourceCountCache myResourceCountsCache;

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
@ -98,6 +99,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
assertEquals(myObservationIds.subList(0, 20), interceptedResourceIds);
// Fetch the next 30 (do cross a fetch boundary)
outcome = myPagingProvider.retrieveResultList(mySrd, outcome.getUuid());
resources = outcome.getResources(10, 40);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(10, 40), returnedIdValues);
@ -126,12 +128,22 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(0, 10), returnedIdValues);
assertEquals(1, hitCount.get());
assertEquals(myObservationIds.subList(0, 20), interceptedResourceIds);
assertEquals("Wrong response from " + outcome.getClass(), myObservationIds.subList(0, 20), interceptedResourceIds);
// Fetch the next 30 (do cross a fetch boundary)
String searchId = outcome.getUuid();
outcome = myPagingProvider.retrieveResultList(mySrd, searchId);
resources = outcome.getResources(10, 40);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(10, 25), returnedIdValues);
if (!myObservationIdsEvenOnly.subList(10,25).equals(returnedIdValues)) {
if (resources.size() != 1) {
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).get();
fail("Failed to load - " + mySearchResultDao.countForSearch(search.getId()) + " results in " + search);
});
}
}
assertEquals("Wrong response from " + outcome.getClass(), myObservationIdsEvenOnly.subList(10, 25), returnedIdValues);
assertEquals(2, hitCount.get());
}
@ -304,6 +316,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
outcome = myPagingProvider.retrieveResultList(mySrd, outcome.getUuid());
List<IBaseResource> resources = outcome.getResources(0, 10);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
ourLog.info("Returned values: {}", returnedIdValues);

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
@ -56,8 +58,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
@Autowired
MatchUrlService myMatchUrlService;
@Autowired
private ISearchDao mySearchEntityDao;
@After
public void afterResetSearchSize() {
@ -1326,7 +1326,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
}
@Test
public void testSearchDateWrongParam() {
Patient p1 = new Patient();
@ -1372,6 +1371,14 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
String searchId = found.getUuid();
for (int i = 0; i < 9; i++) {
List<IBaseResource> resources = found.getResources(i, i + 1);
if (resources.size() != 1) {
int finalI = i;
int finalI1 = i;
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).get();
fail("Failed to load range " + finalI + " - " + (finalI1 + 1) + " - " + mySearchResultDao.countForSearch(search.getId()) + " results in " + search);
});
}
assertThat("Failed to load range " + i + " - " + (i + 1) + " - from provider of type: " + found.getClass(), resources, hasSize(1));
Patient nextResource = (Patient) resources.get(0);
dates.add(nextResource.getBirthDateElement().getValueAsString());

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
@ -142,8 +143,9 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(201, results.size().intValue());
ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, hasSize(10));
IBundleProvider bundleProvider = myDatabaseBackedPagingProvider.retrieveResultList(null, uuid);
PersistedJpaBundleProvider bundleProvider = (PersistedJpaBundleProvider) myDatabaseBackedPagingProvider.retrieveResultList(null, uuid);
Integer bundleSize = bundleProvider.size();
assertNotNull("Null size from provider of type " + bundleProvider.getClass() + " - Cache hit: " + bundleProvider.isCacheHit(), bundleSize);
assertEquals(201, bundleSize.intValue());
// Search with count only

View File

@ -324,7 +324,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
}
});
DatabaseSearchCacheSvcImpl.setNowForUnitTests(search3timestamp.get() + 1100);
DatabaseSearchCacheSvcImpl.setNowForUnitTests(search3timestamp.get() + 2100);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() {

View File

@ -52,7 +52,7 @@ public class ModifyColumnTask extends BaseTableColumnTypeTask<ModifyColumnTask>
throw new InternalErrorException(e);
}
if (isNoColumnShrink()) {
if (getColumnLength() != null && isNoColumnShrink()) {
long existingLength = existingType.getLength() != null ? existingType.getLength() : 0;
if (existingLength > getColumnLength()) {
setColumnLength(existingLength);

View File

@ -63,6 +63,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
// HFJ_SEARCH
version.onTable("HFJ_SEARCH").addColumn("EXPIRY_OR_NULL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP);
version.onTable("HFJ_SEARCH").addColumn("NUM_BLOCKED").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT);
// HFJ_BLK_EXPORT_JOB
version.addIdGenerator("SEQ_BLKEXJOB_PID");

View File

@ -98,6 +98,45 @@ public class ModifyColumnTest extends BaseTest {
}
@Test
public void testNoShrink_ColumnMakeDateNullable() throws SQLException {
executeSql("create table SOMETABLE (PID bigint not null, DATECOL timestamp not null)");
assertFalse(JdbcUtils.isColumnNullable(getConnectionProperties(), "SOMETABLE", "PID"));
assertFalse(JdbcUtils.isColumnNullable(getConnectionProperties(), "SOMETABLE", "DATECOL"));
assertEquals(new JdbcUtils.ColumnType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG, 19), JdbcUtils.getColumnType(getConnectionProperties(), "SOMETABLE", "PID"));
assertEquals(new JdbcUtils.ColumnType(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP, 26), JdbcUtils.getColumnType(getConnectionProperties(), "SOMETABLE", "DATECOL"));
getMigrator().setNoColumnShrink(true);
// PID
ModifyColumnTask task = new ModifyColumnTask();
task.setTableName("SOMETABLE");
task.setColumnName("PID");
task.setColumnType(AddColumnTask.ColumnTypeEnum.LONG);
task.setNullable(true);
getMigrator().addTask(task);
// STRING
task = new ModifyColumnTask();
task.setTableName("SOMETABLE");
task.setColumnName("DATECOL");
task.setColumnType(AddColumnTask.ColumnTypeEnum.DATE_TIMESTAMP);
task.setNullable(true);
getMigrator().addTask(task);
// Do migration
getMigrator().migrate();
assertTrue(JdbcUtils.isColumnNullable(getConnectionProperties(), "SOMETABLE", "PID"));
assertTrue(JdbcUtils.isColumnNullable(getConnectionProperties(), "SOMETABLE", "DATECOL"));
assertEquals(new JdbcUtils.ColumnType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG, 19), JdbcUtils.getColumnType(getConnectionProperties(), "SOMETABLE", "PID"));
assertEquals(new JdbcUtils.ColumnType(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP, 26), JdbcUtils.getColumnType(getConnectionProperties(), "SOMETABLE", "DATECOL"));
// Make sure additional migrations don't crash
getMigrator().migrate();
getMigrator().migrate();
}
@Test
public void testColumnMakeNotNullable() throws SQLException {
executeSql("create table SOMETABLE (PID bigint, TEXTCOL varchar(255))");

View File

@ -56,14 +56,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -77,14 +77,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -19,15 +19,6 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.lang.reflect.Method;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
@ -44,27 +35,38 @@ import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
private static final Set<String> SPECIAL_SEARCH_PARAMS;
private String myCompartmentName;
private String myDescription;
private Integer myIdParamIndex;
private String myQueryName;
private boolean myAllowUnknownParams;
private final String myResourceProviderResourceName;
static {
HashSet<String> specialSearchParams = new HashSet<>();
specialSearchParams.add(IAnyResource.SP_RES_ID);
specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE);
specialSearchParams.add(Constants.PARAM_INCLUDE);
specialSearchParams.add(Constants.PARAM_REVINCLUDE);
SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams);
}
private final String myResourceProviderResourceName;
private String myCompartmentName;
private String myDescription;
private Integer myIdParamIndex;
private String myQueryName;
private boolean myAllowUnknownParams;
public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
super(theReturnResourceType, theMethod, theContext, theProvider);
Search search = theMethod.getAnnotation(Search.class);
@ -90,11 +92,11 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
throw new ConfigurationException(msg);
}
if (theResourceProviderResourceType != null) {
this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName();
} else {
this.myResourceProviderResourceName = null;
}
if (theResourceProviderResourceType != null) {
this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName();
} else {
this.myResourceProviderResourceName = null;
}
}
@ -106,8 +108,8 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return myQueryName;
}
public String getResourceProviderResourceName() {
return myResourceProviderResourceName;
public String getResourceProviderResourceName() {
return myResourceProviderResourceName;
}
@Nonnull
@ -123,28 +125,14 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
@Override
public ReturnTypeEnum getReturnType() {
return ReturnTypeEnum.BUNDLE;
return ReturnTypeEnum.BUNDLE;
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
String clientPreference = theRequest.getHeader(Constants.HEADER_PREFER);
boolean lenientHandling = false;
if(clientPreference != null)
{
String[] preferences = clientPreference.split(";");
for( String p : preferences){
if("handling:lenient".equalsIgnoreCase(p))
{
lenientHandling = true;
break;
}
}
}
if (theRequest.getId() != null && myIdParamIndex == null) {
ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId());
ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId());
return false;
}
if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
@ -156,40 +144,39 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return false;
}
if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
ourLog.trace("Method {} doesn't match because request type is {}", getMethod());
ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType());
return false;
}
if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() });
ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName());
return false;
}
// This is used to track all the parameters so we can reject queries that
// have additional params we don't understand
Set<String> methodParamsTemp = new HashSet<String>();
Set<String> methodParamsTemp = new HashSet<>();
Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
for (int i = 0; i < this.getParameters().size(); i++) {
if (!(getParameters().get(i) instanceof BaseQueryParameter)) {
for (IParameter nextParameter : getParameters()) {
if (!(nextParameter instanceof BaseQueryParameter)) {
continue;
}
BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i);
String name = temp.getName();
if (temp.isRequired()) {
BaseQueryParameter nextQueryParameter = (BaseQueryParameter) nextParameter;
String name = nextQueryParameter.getName();
if (nextQueryParameter.isRequired()) {
if (qualifiedParamNames.contains(name)) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
methodParamsTemp.add(name);
}
}
if (unqualifiedNames.contains(name)) {
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
}
if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name))
{
if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) {
ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
return false;
}
@ -197,16 +184,16 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} else {
if (qualifiedParamNames.contains(name)) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
methodParamsTemp.add(name);
}
}
}
if (unqualifiedNames.contains(name)) {
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
}
if (!qualifiedParamNames.contains(name)) {
if (!qualifiedParamNames.contains(name)) {
methodParamsTemp.add(name);
}
}
@ -237,8 +224,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
}
}
Set<String> keySet = theRequest.getParameters().keySet();
if(lenientHandling == true)
return true;
if (myAllowUnknownParams == false) {
for (String next : keySet) {
@ -272,7 +257,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
return theQualifiedNames;
}
ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
ArrayList<String> retVal = new ArrayList<>(theQualifiedNames.size());
for (String next : theQualifiedNames) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
@ -287,6 +272,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
public String toString() {
return getMethod().toString();
}
public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
QualifierDetails retVal = new QualifierDetails();
if (theParamName == null || theParamName.length() == 0) {

View File

@ -0,0 +1,111 @@
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import com.google.common.collect.ImmutableMap;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SearchMethodBindingTest {
private static final TestResourceProvider TEST_RESOURCE_PROVIDER = new TestResourceProvider();
private FhirContext fhirContext;
@Before
public void setUp() {
fhirContext = mock(FhirContext.class);
RuntimeResourceDefinition definition = mock(RuntimeResourceDefinition.class);
when(definition.isBundle()).thenReturn(false);
when(fhirContext.getResourceDefinition(any(Class.class))).thenReturn(definition);
}
@Test // fails
public void methodShouldNotMatchWhenUnderscoreQueryParameter() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
}
@Test
public void methodShouldNotMatchWhenExtraQueryParameter() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
}
@Test
public void methodMatchesOwnParams() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}))),
Matchers.is(true));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "test", new String[]{"test"}))),
Matchers.is(true));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_test", new String[]{"test"}))),
Matchers.is(true));
}
private SearchMethodBinding getBinding(String name, Class<?>... parameters) throws NoSuchMethodException {
return new SearchMethodBinding(IBaseResource.class,
IBaseResource.class,
TestResourceProvider.class.getMethod(name, parameters),
fhirContext,
TEST_RESOURCE_PROVIDER);
}
private RequestDetails mockSearchRequest(Map<String, String[]> params) {
RequestDetails requestDetails = mock(RequestDetails.class);
when(requestDetails.getOperation()).thenReturn("_search");
when(requestDetails.getRequestType()).thenReturn(RequestTypeEnum.GET);
when(requestDetails.getParameters()).thenReturn(params);
return requestDetails;
}
private static class TestResourceProvider {
@Search
public IBaseResource param(@RequiredParam(name = "param") String param) {
return null;
}
@Search
public IBaseResource paramAndTest(@RequiredParam(name = "param") String param, @OptionalParam(name = "test") String test) {
return null;
}
@Search
public IBaseResource paramAndUnderscoreTest(@RequiredParam(name = "param") String param, @OptionalParam(name = "_test") String test) {
return null;
}
}
}

View File

@ -171,14 +171,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -255,18 +255,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -229,18 +229,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<!--<classFolders>-->
<!--<classFolder>${basedir}/target/classes</classFolder>-->
<!--<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>-->
<!--<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>-->
<!--<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>-->
<!--</classFolders>-->
<!--<sourceFolders>-->
<!--<sourceFolder>${basedir}/src/main/java</sourceFolder>-->
<!--<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>-->
<!--<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>-->
<!--<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>-->
<!--</sourceFolders>-->
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -148,6 +148,7 @@ public class FhirContextDstu2Test {
afterInitBlocker.await();
submittedTestRunnable.run();
} catch (final Throwable e) {
ourLog.error("Exception", e);
exceptions.add(e);
} finally {
allDone.countDown();

View File

@ -254,18 +254,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}../hapi-fhir-base/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -296,18 +296,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -0,0 +1,190 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.test.utilities.JettyUtil;
import com.google.common.collect.Lists;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.After;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;
public class ServerMethodSelectionR4Test {
private FhirContext myCtx = FhirContext.forR4();
private Server myServer;
private IGenericClient myClient;
@After
public void after() throws Exception {
JettyUtil.closeServer(myServer);
}
/**
* Server method with no _include
* Client request with _include
* <p>
* See #1421
*/
@Test
public void testRejectIncludeIfNotProvided() throws Exception {
class MyProvider extends MyBaseProvider {
@Search
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName) {
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
}
}
MyProvider provider = new MyProvider();
startServer(provider);
try {
myClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("foo"))
.include(Patient.INCLUDE_ORGANIZATION)
.execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("this server does not know how to handle GET operation[Patient] with parameters [[_include, name]]"));
}
}
/**
* Server method with no _include
* Client request with _include
* <p>
* See #1421
*/
@Test
public void testAllowIncludeIfProvided() throws Exception {
class MyProvider extends MyBaseProvider {
@Search
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName, @IncludeParam Set<Include> theIncludes) {
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
}
}
MyProvider provider = new MyProvider();
startServer(provider);
Bundle results = myClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("foo"))
.include(Patient.INCLUDE_ORGANIZATION)
.returnBundle(Bundle.class)
.execute();
assertEquals(1, results.getEntry().size());
}
/**
* Server method with no _revinclude
* Client request with _revinclude
* <p>
* See #1421
*/
@Test
public void testRejectRevIncludeIfNotProvided() throws Exception {
class MyProvider extends MyBaseProvider {
@Search
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName) {
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
}
}
MyProvider provider = new MyProvider();
startServer(provider);
try {
myClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("foo"))
.revInclude(Patient.INCLUDE_ORGANIZATION)
.execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("this server does not know how to handle GET operation[Patient] with parameters [[_revinclude, name]]"));
}
}
/**
* Server method with no _revInclude
* Client request with _revInclude
* <p>
* See #1421
*/
@Test
public void testAllowRevIncludeIfProvided() throws Exception {
class MyProvider extends MyBaseProvider {
@Search
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName, @IncludeParam(reverse = true) Set<Include> theRevIncludes) {
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
}
}
MyProvider provider = new MyProvider();
startServer(provider);
Bundle results = myClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("foo"))
.revInclude(Patient.INCLUDE_ORGANIZATION)
.returnBundle(Bundle.class)
.execute();
assertEquals(1, results.getEntry().size());
}
private void startServer(Object theProvider) throws Exception {
RestfulServer servlet = new RestfulServer(myCtx);
servlet.registerProvider(theProvider);
ServletHandler proxyHandler = new ServletHandler();
servlet.setDefaultResponseEncoding(EncodingEnum.XML);
servlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
myServer = new Server(0);
myServer.setHandler(proxyHandler);
JettyUtil.startServer(myServer);
int port = JettyUtil.getPortForStartedServer(myServer);
myClient = myCtx.newRestfulGenericClient("http://localhost:" + port);
}
public static class MyBaseProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
}
}

View File

@ -289,18 +289,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -254,18 +254,6 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-client/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-server/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-client/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-server/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>

View File

@ -607,7 +607,7 @@
<httpcore_version>4.4.11</httpcore_version>
<httpclient_version>4.5.9</httpclient_version>
<jackson_version>2.9.9</jackson_version>
<jackson_databind_version>2.9.9.1</jackson_databind_version>
<jackson_databind_version>2.9.10</jackson_databind_version>
<maven_assembly_plugin_version>3.1.0</maven_assembly_plugin_version>
<maven_license_plugin_version>1.8</maven_license_plugin_version>
<resteasy_version>4.0.0.Beta3</resteasy_version>

View File

@ -13,6 +13,7 @@
<![CDATA[
<ul>
<li>Hibernate Core (Core): 5.4.2.Final -&gt; 5.4.4.Final</li>
<li>Jackson Databind (JPA): 2.9.9 -&gt; 2.9.10 (CVE-2019-16335, CVE-2019-14540)</li>
</ul>
]]>
</action>
@ -212,6 +213,11 @@
with the new request id, resulting in an ever growing source.meta value. E.g. after the first update, it looks
like "#9f0a901387128111#5f37835ee38a89e2" when it should only be "#5f37835ee38a89e2". This has been corrected.
</action>
<action type="fix" issue="1421">
The Plain Server method selector was incorrectly allowing client requests with _include statements to be
handled by method implementations that did not have any <![CDATA[<code>@IncludeParam</code>]]> defined. This
is now corrected. Thanks to Tuomo Ala-Vannesluoma for reporting and providing a test case!
</action>
</release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix">