Integrate Bulk Export (#1487)

* Start working on subscription processor

* Work on new scheduler

* Test fixes

* Scheduler refactoring

* Fix test failure

* One more test fix

* Updates to scheduler

* More scheduler work

* Tests now all passing

* Ongoing work on export

* Ongoing scheduler work

* Ongoing testing

* Work on export task

* Sync master

* Ongoing work

* Bump xml patch version

* Work on provider

* Work on bulk

* Work on export scheduler

* More test fies

* More test fixes

* Compile fix

* Reduce logging

* Improve logging

* Reuse bulk export jobs

* Export provider

* Improve logging in bulk export

* Work on bulk export service

* One more bugfix

* Ongoing work on Bulk Data

* Add changelog
This commit is contained in:
James Agnew 2019-09-17 16:01:35 -04:00 committed by GitHub
parent 882e0853df
commit 4a751cbfc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 4152 additions and 648 deletions

View File

@ -8,6 +8,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import ca.uhn.fhir.rest.api.PreferHeader;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.junit.*; import org.junit.*;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@ -20,7 +21,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
@ -246,7 +246,7 @@ public class GenericClientDstu3IT {
Patient pt = new Patient(); Patient pt = new Patient();
pt.getText().setDivAsString("A PATIENT"); pt.getText().setDivAsString("A PATIENT");
MethodOutcome outcome = client.create().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute(); MethodOutcome outcome = client.create().resource(pt).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertNull(outcome.getOperationOutcome()); assertNull(outcome.getOperationOutcome());
assertNotNull(outcome.getResource()); assertNotNull(outcome.getResource());

View File

@ -220,17 +220,31 @@ public class Constants {
public static final String CASCADE_DELETE = "delete"; public static final String CASCADE_DELETE = "delete";
public static final int MAX_RESOURCE_NAME_LENGTH = 100; public static final int MAX_RESOURCE_NAME_LENGTH = 100;
public static final String CACHE_CONTROL_PRIVATE = "private"; public static final String CACHE_CONTROL_PRIVATE = "private";
public static final String CT_FHIR_NDJSON = "application/fhir+ndjson";
public static final String CT_APP_NDJSON = "application/ndjson";
public static final String CT_NDJSON = "ndjson";
public static final Set<String> CTS_NDJSON;
public static final String HEADER_PREFER_RESPOND_ASYNC = "respond-async";
public static final int STATUS_HTTP_412_PAYLOAD_TOO_LARGE = 413; public static final int STATUS_HTTP_412_PAYLOAD_TOO_LARGE = 413;
public static final String OPERATION_NAME_GRAPHQL = "$graphql"; public static final String OPERATION_NAME_GRAPHQL = "$graphql";
/** /**
* Note that this constant is used in a number of places including DB column lengths! Be careful if you decide to change it. * Note that this constant is used in a number of places including DB column lengths! Be careful if you decide to change it.
*/ */
public static final int REQUEST_ID_LENGTH = 16; public static final int REQUEST_ID_LENGTH = 16;
public static final int STATUS_HTTP_202_ACCEPTED = 202;
public static final String HEADER_X_PROGRESS = "X-Progress";
public static final String HEADER_RETRY_AFTER = "Retry-After";
static { static {
CHARSET_UTF8 = StandardCharsets.UTF_8; CHARSET_UTF8 = StandardCharsets.UTF_8;
CHARSET_US_ASCII = StandardCharsets.ISO_8859_1; CHARSET_US_ASCII = StandardCharsets.ISO_8859_1;
HashSet<String> ctsNdjson = new HashSet<>();
ctsNdjson.add(CT_FHIR_NDJSON);
ctsNdjson.add(CT_APP_NDJSON);
ctsNdjson.add(CT_NDJSON);
CTS_NDJSON = Collections.unmodifiableSet(ctsNdjson);
HashMap<Integer, String> statusNames = new HashMap<>(); HashMap<Integer, String> statusNames = new HashMap<>();
statusNames.put(200, "OK"); statusNames.put(200, "OK");
statusNames.put(201, "Created"); statusNames.put(201, "Created");

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.rest.api;
import javax.annotation.Nullable;
import java.util.HashMap;
public class PreferHeader {
private PreferReturnEnum myReturn;
private boolean myRespondAsync;
public @Nullable
PreferReturnEnum getReturn() {
return myReturn;
}
public PreferHeader setReturn(PreferReturnEnum theReturn) {
myReturn = theReturn;
return this;
}
public boolean getRespondAsync() {
return myRespondAsync;
}
public PreferHeader setRespondAsync(boolean theRespondAsync) {
myRespondAsync = theRespondAsync;
return this;
}
/**
* Represents values for "return" value as provided in the the <a href="https://tools.ietf.org/html/rfc7240#section-4.2">HTTP Prefer header</a>.
*/
public enum PreferReturnEnum {
REPRESENTATION("representation"), MINIMAL("minimal"), OPERATION_OUTCOME("OperationOutcome");
private static HashMap<String, PreferReturnEnum> ourValues;
private String myHeaderValue;
PreferReturnEnum(String theHeaderValue) {
myHeaderValue = theHeaderValue;
}
public String getHeaderValue() {
return myHeaderValue;
}
public static PreferReturnEnum fromHeaderValue(String theHeaderValue) {
if (ourValues == null) {
HashMap<String, PreferReturnEnum> values = new HashMap<>();
for (PreferReturnEnum next : PreferReturnEnum.values()) {
values.put(next.getHeaderValue(), next);
}
ourValues = values;
}
return ourValues.get(theHeaderValue);
}
}
}

View File

@ -1,54 +0,0 @@
package ca.uhn.fhir.rest.api;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.HashMap;
/**
* Represents values for "return" value as provided in the the <a href="https://tools.ietf.org/html/rfc7240#section-4.2">HTTP Prefer header</a>.
*/
public enum PreferReturnEnum {
REPRESENTATION("representation"), MINIMAL("minimal"), OPERATION_OUTCOME("OperationOutcome");
private String myHeaderValue;
private static HashMap<String, PreferReturnEnum> ourValues;
private PreferReturnEnum(String theHeaderValue) {
myHeaderValue = theHeaderValue;
}
public static PreferReturnEnum fromHeaderValue(String theHeaderValue) {
if (ourValues == null) {
HashMap<String, PreferReturnEnum> values = new HashMap<String, PreferReturnEnum>();
for (PreferReturnEnum next : PreferReturnEnum.values()) {
values.put(next.getHeaderValue(), next);
}
ourValues = values;
}
return ourValues.get(theHeaderValue);
}
public String getHeaderValue() {
return myHeaderValue;
}
}

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.gclient;
*/ */
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferHeader;
public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutcome> { public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutcome> {
@ -47,6 +47,6 @@ public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutc
* *
* @since HAPI 1.1 * @since HAPI 1.1
*/ */
ICreateTyped prefer(PreferReturnEnum theReturn); ICreateTyped prefer(PreferHeader.PreferReturnEnum theReturn);
} }

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.gclient;
*/ */
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferHeader;
public interface IPatchExecutable extends IClientExecutable<IPatchExecutable, MethodOutcome>{ public interface IPatchExecutable extends IClientExecutable<IPatchExecutable, MethodOutcome>{
@ -32,6 +32,6 @@ public interface IPatchExecutable extends IClientExecutable<IPatchExecutable, Me
* *
* @since HAPI 1.1 * @since HAPI 1.1
*/ */
IPatchExecutable prefer(PreferReturnEnum theReturn); IPatchExecutable prefer(PreferHeader.PreferReturnEnum theReturn);
} }

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.gclient;
*/ */
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferHeader;
public interface IUpdateExecutable extends IClientExecutable<IUpdateExecutable, MethodOutcome>{ public interface IUpdateExecutable extends IClientExecutable<IUpdateExecutable, MethodOutcome>{
@ -32,6 +32,6 @@ public interface IUpdateExecutable extends IClientExecutable<IUpdateExecutable,
* *
* @since HAPI 1.1 * @since HAPI 1.1
*/ */
IUpdateExecutable prefer(PreferReturnEnum theReturn); IUpdateExecutable prefer(PreferHeader.PreferReturnEnum theReturn);
} }

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.util;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*;
public class ArrayUtil {
/** Non instantiable */
private ArrayUtil() {}
/**
* Takes in a list like "foo, bar,, baz" and returns a set containing only ["foo", "bar", "baz"]
*/
public static Set<String> commaSeparatedListToCleanSet(String theValueAsString) {
Set<String> resourceTypes;
resourceTypes = Arrays.stream(split(theValueAsString, ","))
.map(t->trim(t))
.filter(t->isNotBlank(t))
.collect(Collectors.toSet());
return resourceTypes;
}
}

View File

@ -63,6 +63,9 @@ public class StopWatch {
myStarted = theStart.getTime(); myStarted = theStart.getTime();
} }
public StopWatch(long theL) {
}
private void addNewlineIfContentExists(StringBuilder theB) { private void addNewlineIfContentExists(StringBuilder theB) {
if (theB.length() > 0) { if (theB.length() > 0) {
theB.append("\n"); theB.append("\n");
@ -231,7 +234,12 @@ public class StopWatch {
double denominator = ((double) millisElapsed) / ((double) periodMillis); double denominator = ((double) millisElapsed) / ((double) periodMillis);
return (double) theNumOperations / denominator; double throughput = (double) theNumOperations / denominator;
if (throughput > theNumOperations) {
throughput = theNumOperations;
}
return throughput;
} }
public void restart() { public void restart() {

View File

@ -5,9 +5,9 @@
if you are using this file as a basis for your own project. --> if you are using this file as a basis for your own project. -->
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>4.1.0-SNAPSHOT</version> <version>4.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../../hapi-deployable-pom</relativePath>
</parent> </parent>
<artifactId>hapi-fhir-cli-jpaserver</artifactId> <artifactId>hapi-fhir-cli-jpaserver</artifactId>
@ -131,6 +131,10 @@
<artifactId>Saxon-HE</artifactId> <artifactId>Saxon-HE</artifactId>
<groupId>net.sf.saxon</groupId> <groupId>net.sf.saxon</groupId>
</exclusion> </exclusion>
<exclusion>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-core</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
</dependencies> </dependencies>
@ -183,15 +187,6 @@
</configuration> </configuration>
</plugin> </plugin>
<!-- This plugin is just a part of the HAPI internal build process, you do not need to incude it in your own projects -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<!-- This is to run the integration tests --> <!-- This is to run the integration tests -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2; import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.config.BaseJavaConfigR4; import ca.uhn.fhir.jpa.config.BaseJavaConfigR4;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorR4; import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorR4;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.demo; package ca.uhn.fhir.jpa.demo;
/*-
* #%L
* HAPI FHIR - Command Line Client - Server WAR
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;

View File

@ -364,11 +364,11 @@ public class GenericOkHttpClientDstu2Test {
Patient p = new Patient(); Patient p = new Patient();
p.addName().addFamily("FOOFAMILY"); p.addName().addFamily("FOOFAMILY");
client.create().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.create().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
} }
@ -1735,11 +1735,11 @@ public class GenericOkHttpClientDstu2Test {
p.setId(new IdDt("1")); p.setId(new IdDt("1"));
p.addName().addFamily("FOOFAMILY"); p.addName().addFamily("FOOFAMILY");
client.update().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.update().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
} }

View File

@ -0,0 +1,20 @@
package ca.uhn.fhir.rest.client.apache;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.nio.charset.UnsupportedCharsetException;
/**
* Apache HttpClient request content entity where the body is a FHIR resource, that will
* be encoded as JSON by default
*/
public class ResourceEntity extends StringEntity {
public ResourceEntity(FhirContext theContext, IBaseResource theResource) throws UnsupportedCharsetException {
super(theContext.newJsonParser().encodeResourceToString(theResource), ContentType.parse(Constants.CT_FHIR_JSON_NEW));
}
}

View File

@ -533,7 +533,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private class CreateInternal extends BaseSearch<ICreateTyped, ICreateWithQueryTyped, MethodOutcome> implements ICreate, ICreateTyped, ICreateWithQuery, ICreateWithQueryTyped { private class CreateInternal extends BaseSearch<ICreateTyped, ICreateWithQueryTyped, MethodOutcome> implements ICreate, ICreateTyped, ICreateWithQuery, ICreateWithQueryTyped {
private boolean myConditional; private boolean myConditional;
private PreferReturnEnum myPrefer; private PreferHeader.PreferReturnEnum myPrefer;
private IBaseResource myResource; private IBaseResource myResource;
private String myResourceBody; private String myResourceBody;
private String mySearchUrl; private String mySearchUrl;
@ -580,7 +580,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
@Override @Override
public ICreateTyped prefer(PreferReturnEnum theReturn) { public ICreateTyped prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn; myPrefer = theReturn;
return this; return this;
} }
@ -1380,13 +1380,13 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> { private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private PreferReturnEnum myPrefer; private PreferHeader.PreferReturnEnum myPrefer;
private OutcomeResponseHandler() { private OutcomeResponseHandler() {
super(); super();
} }
private OutcomeResponseHandler(PreferReturnEnum thePrefer) { private OutcomeResponseHandler(PreferHeader.PreferReturnEnum thePrefer) {
this(); this();
myPrefer = thePrefer; myPrefer = thePrefer;
} }
@ -1396,7 +1396,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders); MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
response.setCreatedUsingStatusCode(theResponseStatusCode); response.setCreatedUsingStatusCode(theResponseStatusCode);
if (myPrefer == PreferReturnEnum.REPRESENTATION) { if (myPrefer == PreferHeader.PreferReturnEnum.REPRESENTATION) {
if (response.getResource() == null) { if (response.getResource() == null) {
if (response.getId() != null && isNotBlank(response.getId().getValue()) && response.getId().hasBaseUrl()) { if (response.getId() != null && isNotBlank(response.getId().getValue()) && response.getId().hasBaseUrl()) {
ourLog.info("Server did not return resource for Prefer-representation, going to fetch: {}", response.getId().getValue()); ourLog.info("Server did not return resource for Prefer-representation, going to fetch: {}", response.getId().getValue());
@ -1418,7 +1418,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private IIdType myId; private IIdType myId;
private String myPatchBody; private String myPatchBody;
private PatchTypeEnum myPatchType; private PatchTypeEnum myPatchType;
private PreferReturnEnum myPrefer; private PreferHeader.PreferReturnEnum myPrefer;
private String myResourceType; private String myResourceType;
private String mySearchUrl; private String mySearchUrl;
@ -1476,7 +1476,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
@Override @Override
public IPatchExecutable prefer(PreferReturnEnum theReturn) { public IPatchExecutable prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn; myPrefer = theReturn;
return this; return this;
} }
@ -2048,7 +2048,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private boolean myConditional; private boolean myConditional;
private IIdType myId; private IIdType myId;
private PreferReturnEnum myPrefer; private PreferHeader.PreferReturnEnum myPrefer;
private IBaseResource myResource; private IBaseResource myResource;
private String myResourceBody; private String myResourceBody;
private String mySearchUrl; private String mySearchUrl;
@ -2102,7 +2102,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
@Override @Override
public IUpdateExecutable prefer(PreferReturnEnum theReturn) { public IUpdateExecutable prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn; myPrefer = theReturn;
return this; return this;
} }
@ -2282,7 +2282,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
params.get(parameterName).add(parameterValue); params.get(parameterName).add(parameterValue);
} }
private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) { private static void addPreferHeader(PreferHeader.PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) {
if (thePrefer != null) { if (thePrefer != null) {
theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue()); theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue());
} }

View File

@ -112,8 +112,8 @@ public abstract class AbstractJaxRsPageProvider extends AbstractJaxRsProvider im
} }
@Override @Override
public PreferReturnEnum getDefaultPreferReturn() { public PreferHeader.PreferReturnEnum getDefaultPreferReturn() {
return PreferReturnEnum.REPRESENTATION; return PreferHeader.PreferReturnEnum.REPRESENTATION;
} }
} }

View File

@ -28,7 +28,6 @@ import javax.ws.rs.*;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -371,8 +370,8 @@ implements IRestfulServer<JaxRsRequest>, IResourceProvider {
} }
@Override @Override
public PreferReturnEnum getDefaultPreferReturn() { public PreferHeader.PreferReturnEnum getDefaultPreferReturn() {
return PreferReturnEnum.REPRESENTATION; return PreferHeader.PreferReturnEnum.REPRESENTATION;
} }
/** /**

View File

@ -301,12 +301,12 @@ public class GenericJaxRsClientDstu2Test {
Patient p = new Patient(); Patient p = new Patient();
p.addName().addFamily("FOOFAMILY"); p.addName().addFamily("FOOFAMILY");
client.create().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.create().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
@ -1927,12 +1927,12 @@ public class GenericJaxRsClientDstu2Test {
p.setId(new IdDt("1")); p.setId(new IdDt("1"));
p.addName().addFamily("FOOFAMILY"); p.addName().addFamily("FOOFAMILY");
client.update().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.update().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());

View File

@ -321,12 +321,12 @@ public class GenericJaxRsClientDstu3Test {
Patient p = new Patient(); Patient p = new Patient();
p.addName().setFamily("FOOFAMILY"); p.addName().setFamily("FOOFAMILY");
client.create().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.create().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.create().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
@ -1980,12 +1980,12 @@ public class GenericJaxRsClientDstu3Test {
p.setId(new IdType("1")); p.setId(new IdType("1"));
p.addName().setFamily("FOOFAMILY"); p.addName().setFamily("FOOFAMILY");
client.update().resource(p).prefer(PreferReturnEnum.MINIMAL).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.MINIMAL).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_MINIMAL, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());
client.update().resource(p).prefer(PreferReturnEnum.REPRESENTATION).execute(); client.update().resource(p).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size()); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_PREFER).size());
assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue()); assertEquals(Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION, ourRequestHeaders.get(Constants.HEADER_PREFER).get(0).getValue());

View File

@ -7,10 +7,7 @@ import ca.uhn.fhir.jaxrs.server.test.TestJaxRsConformanceRestProviderDstu3;
import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPageProviderDstu3; import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPageProviderDstu3;
import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProviderDstu3; import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProviderDstu3;
import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -138,7 +135,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
MethodOutcome response = client.create().resource(toCreate).conditional() MethodOutcome response = client.create().resource(toCreate).conditional()
.where(Patient.IDENTIFIER.exactly().identifier("2")).prefer(PreferReturnEnum.REPRESENTATION).execute(); .where(Patient.IDENTIFIER.exactly().identifier("2")).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals("myIdentifier", patientCaptor.getValue().getIdentifier().get(0).getValue()); assertEquals("myIdentifier", patientCaptor.getValue().getIdentifier().get(0).getValue());
IBaseResource resource = response.getResource(); IBaseResource resource = response.getResource();
@ -161,7 +158,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome); when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome);
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
final MethodOutcome response = client.create().resource(toCreate).prefer(PreferReturnEnum.REPRESENTATION) final MethodOutcome response = client.create().resource(toCreate).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION)
.execute(); .execute();
IBaseResource resource = response.getResource(); IBaseResource resource = response.getResource();
compareResultId(1, resource); compareResultId(1, resource);

View File

@ -13,10 +13,7 @@ import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -134,7 +131,7 @@ public class AbstractJaxRsResourceProviderTest {
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
MethodOutcome response = client.create().resource(toCreate).conditional() MethodOutcome response = client.create().resource(toCreate).conditional()
.where(Patient.IDENTIFIER.exactly().identifier("2")).prefer(PreferReturnEnum.REPRESENTATION).execute(); .where(Patient.IDENTIFIER.exactly().identifier("2")).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
assertEquals("myIdentifier", patientCaptor.getValue().getIdentifierFirstRep().getValue()); assertEquals("myIdentifier", patientCaptor.getValue().getIdentifierFirstRep().getValue());
IResource resource = (IResource) response.getResource(); IResource resource = (IResource) response.getResource();
@ -157,7 +154,7 @@ public class AbstractJaxRsResourceProviderTest {
when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome); when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome);
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
final MethodOutcome response = client.create().resource(toCreate).prefer(PreferReturnEnum.REPRESENTATION) final MethodOutcome response = client.create().resource(toCreate).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION)
.execute(); .execute();
IResource resource = (IResource) response.getResource(); IResource resource = (IResource) response.getResource();
compareResultId(1, resource); compareResultId(1, resource);

View File

@ -138,7 +138,7 @@ public class JaxRsPatientProviderDstu3Test {
existing.setId((IdType) null); existing.setId((IdType) null);
existing.getName().add(new HumanName().setFamily("Created Patient 54")); existing.getName().add(new HumanName().setFamily("Created Patient 54"));
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
System.out.println(patient); System.out.println(patient);
@ -154,7 +154,7 @@ public class JaxRsPatientProviderDstu3Test {
existing.setId((IdType) null); existing.setId((IdType) null);
existing.getName().add(new HumanName().setFamily("Created Patient 54")); existing.getName().add(new HumanName().setFamily("Created Patient 54"));
client.setEncoding(EncodingEnum.XML); client.setEncoding(EncodingEnum.XML);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
@ -187,7 +187,7 @@ public class JaxRsPatientProviderDstu3Test {
public void testDeletePatient() { public void testDeletePatient() {
final Patient existing = new Patient(); final Patient existing = new Patient();
existing.getName().add(new HumanName().setFamily("Created Patient XYZ")); existing.getName().add(new HumanName().setFamily("Created Patient XYZ"));
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
client.delete().resourceById(patient.getIdElement()).execute(); client.delete().resourceById(patient.getIdElement()).execute();

View File

@ -7,7 +7,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.SearchStyleEnum; import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@ -152,7 +152,7 @@ public class JaxRsPatientProviderR4Test {
existing.setId((IdDt) null); existing.setId((IdDt) null);
existing.getNameFirstRep().setFamily("Created Patient 54"); existing.getNameFirstRep().setFamily("Created Patient 54");
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
System.out.println(patient); System.out.println(patient);
@ -167,7 +167,7 @@ public class JaxRsPatientProviderR4Test {
existing.setId((IdDt) null); existing.setId((IdDt) null);
existing.getNameFirstRep().setFamily("Created Patient 54"); existing.getNameFirstRep().setFamily("Created Patient 54");
client.setEncoding(EncodingEnum.XML); client.setEncoding(EncodingEnum.XML);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
@ -199,7 +199,7 @@ public class JaxRsPatientProviderR4Test {
public void testDeletePatient() { public void testDeletePatient() {
final Patient existing = new Patient(); final Patient existing = new Patient();
existing.getNameFirstRep().setFamily("Created Patient XYZ"); existing.getNameFirstRep().setFamily("Created Patient XYZ");
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
client.delete().resource(patient).execute(); client.delete().resource(patient).execute();

View File

@ -149,7 +149,7 @@ public class JaxRsPatientProviderTest {
existing.setId((IdDt) null); existing.setId((IdDt) null);
existing.getNameFirstRep().addFamily("Created Patient 54"); existing.getNameFirstRep().addFamily("Created Patient 54");
client.setEncoding(EncodingEnum.JSON); client.setEncoding(EncodingEnum.JSON);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
System.out.println(patient); System.out.println(patient);
@ -164,7 +164,7 @@ public class JaxRsPatientProviderTest {
existing.setId((IdDt) null); existing.setId((IdDt) null);
existing.getNameFirstRep().addFamily("Created Patient 54"); existing.getNameFirstRep().addFamily("Created Patient 54");
client.setEncoding(EncodingEnum.XML); client.setEncoding(EncodingEnum.XML);
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
@ -196,7 +196,7 @@ public class JaxRsPatientProviderTest {
public void testDeletePatient() { public void testDeletePatient() {
final Patient existing = new Patient(); final Patient existing = new Patient();
existing.getNameFirstRep().addFamily("Created Patient XYZ"); existing.getNameFirstRep().addFamily("Created Patient XYZ");
final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); final MethodOutcome results = client.create().resource(existing).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute();
System.out.println(results.getId()); System.out.println(results.getId());
final Patient patient = (Patient) results.getResource(); final Patient patient = (Patient) results.getResource();
client.delete().resourceById(patient.getId()).execute(); client.delete().resourceById(patient.getId()).execute();

View File

@ -309,6 +309,11 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.2</version> </dependency> --> <!-- <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.2</version> </dependency> -->
@ -700,7 +705,7 @@
<configPackageBase>ca.uhn.fhir.jpa.config</configPackageBase> <configPackageBase>ca.uhn.fhir.jpa.config</configPackageBase>
<packageBase>ca.uhn.fhir.jpa.rp.dstu2</packageBase> <packageBase>ca.uhn.fhir.jpa.rp.dstu2</packageBase>
<targetResourceSpringBeansFile>hapi-fhir-server-resourceproviders-dstu2.xml</targetResourceSpringBeansFile> <targetResourceSpringBeansFile>hapi-fhir-server-resourceproviders-dstu2.xml</targetResourceSpringBeansFile>
<baseResourceNames></baseResourceNames> <baseResourceNames/>
<excludeResourceNames> <excludeResourceNames>
<!-- <excludeResourceName>OperationDefinition</excludeResourceName> <excludeResourceName>OperationOutcome</excludeResourceName> --> <!-- <excludeResourceName>OperationDefinition</excludeResourceName> <excludeResourceName>OperationOutcome</excludeResourceName> -->
</excludeResourceNames> </excludeResourceNames>

View File

@ -0,0 +1,158 @@
package ca.uhn.fhir.jpa.bulk;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.util.JsonUtil;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ArrayUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.InstantType;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.Set;
public class BulkDataExportProvider {
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Autowired
private FhirContext myFhirContext;
@VisibleForTesting
public void setFhirContextForUnitTest(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@VisibleForTesting
public void setBulkDataExportSvcForUnitTests(IBulkDataExportSvc theBulkDataExportSvc) {
myBulkDataExportSvc = theBulkDataExportSvc;
}
/**
* $export
*/
@Operation(name = JpaConstants.OPERATION_EXPORT, global = false /* set to true once we can handle this */, manualResponse = true, idempotent = true)
public void export(
@OperationParam(name = JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, min = 0, max = 1, typeName = "string") IPrimitiveType<String> theOutputFormat,
@OperationParam(name = JpaConstants.PARAM_EXPORT_TYPE, min = 0, max = 1, typeName = "string") IPrimitiveType<String> theType,
@OperationParam(name = JpaConstants.PARAM_EXPORT_SINCE, min = 0, max = 1, typeName = "instant") IPrimitiveType<Date> theSince,
@OperationParam(name = JpaConstants.PARAM_EXPORT_TYPE_FILTER, min = 0, max = 1, typeName = "string") IPrimitiveType<String> theTypeFilter,
ServletRequestDetails theRequestDetails
) {
String preferHeader = theRequestDetails.getHeader(Constants.HEADER_PREFER);
PreferHeader prefer = RestfulServerUtils.parsePreferHeader(null, preferHeader);
if (prefer.getRespondAsync() == false) {
throw new InvalidRequestException("Must request async processing for $export");
}
String outputFormat = theOutputFormat != null ? theOutputFormat.getValueAsString() : null;
Set<String> resourceTypes = null;
if (theType != null) {
resourceTypes = ArrayUtil.commaSeparatedListToCleanSet(theType.getValueAsString());
}
Date since = null;
if (theSince != null) {
since = theSince.getValue();
}
Set<String> filters = null;
if (theTypeFilter != null) {
filters = ArrayUtil.commaSeparatedListToCleanSet(theTypeFilter.getValueAsString());
}
IBulkDataExportSvc.JobInfo outcome = myBulkDataExportSvc.submitJob(outputFormat, resourceTypes, since, filters);
String serverBase = getServerBase(theRequestDetails);
String pollLocation = serverBase + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" + JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + outcome.getJobId();
HttpServletResponse response = theRequestDetails.getServletResponse();
// Add standard headers
theRequestDetails.getServer().addHeadersToResponse(response);
// Successful 202 Accepted
response.addHeader(Constants.HEADER_CONTENT_LOCATION, pollLocation);
response.setStatus(Constants.STATUS_HTTP_202_ACCEPTED);
}
/**
* $export-poll-status
*/
@Operation(name = JpaConstants.OPERATION_EXPORT_POLL_STATUS, manualResponse = true, idempotent = true)
public void exportPollStatus(
@OperationParam(name = JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID, typeName = "string", min = 0, max = 1) IPrimitiveType<String> theJobId,
ServletRequestDetails theRequestDetails
) throws IOException {
HttpServletResponse response = theRequestDetails.getServletResponse();
theRequestDetails.getServer().addHeadersToResponse(response);
IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(theJobId.getValueAsString());
switch (status.getStatus()) {
case SUBMITTED:
case BUILDING:
response.setStatus(Constants.STATUS_HTTP_202_ACCEPTED);
response.addHeader(Constants.HEADER_X_PROGRESS, "Build in progress - Status set to " + status.getStatus() + " at " + new InstantType(status.getStatusTime()).getValueAsString());
response.addHeader(Constants.HEADER_RETRY_AFTER, "120");
break;
case COMPLETE:
response.setStatus(Constants.STATUS_HTTP_200_OK);
response.setContentType(Constants.CT_JSON);
// Create a JSON response
BulkExportResponseJson bulkResponseDocument = new BulkExportResponseJson();
bulkResponseDocument.setTransactionTime(status.getStatusTime());
bulkResponseDocument.setRequest(status.getRequest());
for (IBulkDataExportSvc.FileEntry nextFile : status.getFiles()) {
String serverBase = getServerBase(theRequestDetails);
String nextUrl = serverBase + "/" + nextFile.getResourceId().toUnqualifiedVersionless().getValue();
bulkResponseDocument
.addOutput()
.setType(nextFile.getResourceType())
.setUrl(nextUrl);
}
JsonUtil.serialize(bulkResponseDocument, response.getWriter());
response.getWriter().close();
break;
case ERROR:
response.setStatus(Constants.STATUS_HTTP_500_INTERNAL_ERROR);
response.setContentType(Constants.CT_FHIR_JSON);
// Create an OperationOutcome response
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(myFhirContext);
OperationOutcomeUtil.addIssue(myFhirContext, oo, "error", status.getStatusMessage(), null, null);
myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToWriter(oo, response.getWriter());
response.getWriter().close();
}
}
private String getServerBase(ServletRequestDetails theRequestDetails) {
return StringUtils.removeEnd(theRequestDetails.getServerBaseForRequest(), "/");
}
}

View File

@ -0,0 +1,447 @@
package ca.uhn.fhir.jpa.bulk;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.IResultIterator;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao;
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao;
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
import ca.uhn.fhir.jpa.model.sched.FireAtIntervalJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.collect.Sets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.InstantType;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class BulkDataExportSvcImpl implements IBulkDataExportSvc {
private static final long REFRESH_INTERVAL = 10 * DateUtils.MILLIS_PER_SECOND;
private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportSvcImpl.class);
private int myReuseBulkExportForMillis = (int) (60 * DateUtils.MILLIS_PER_MINUTE);
@Autowired
private IBulkExportJobDao myBulkExportJobDao;
@Autowired
private IBulkExportCollectionDao myBulkExportCollectionDao;
@Autowired
private IBulkExportCollectionFileDao myBulkExportCollectionFileDao;
@Autowired
private ISchedulerService mySchedulerService;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private FhirContext myContext;
@Autowired
private PlatformTransactionManager myTxManager;
private TransactionTemplate myTxTemplate;
private long myFileMaxChars = 500 * FileUtils.ONE_KB;
private int myRetentionPeriod = (int) DateUtils.MILLIS_PER_DAY;
/**
* This method is called by the scheduler to run a pass of the
* generator
*/
@Transactional(value = Transactional.TxType.NEVER)
@Override
public synchronized void buildExportFiles() {
Optional<BulkExportJobEntity> jobToProcessOpt = myTxTemplate.execute(t -> {
Pageable page = PageRequest.of(0, 1);
Slice<BulkExportJobEntity> submittedJobs = myBulkExportJobDao.findByStatus(page, BulkJobStatusEnum.SUBMITTED);
if (submittedJobs.isEmpty()) {
return Optional.empty();
}
return Optional.of(submittedJobs.getContent().get(0));
});
if (!jobToProcessOpt.isPresent()) {
return;
}
String jobUuid = jobToProcessOpt.get().getJobId();
try {
myTxTemplate.execute(t -> {
processJob(jobUuid);
return null;
});
} catch (Exception e) {
ourLog.error("Failure while preparing bulk export extract", e);
myTxTemplate.execute(t -> {
Optional<BulkExportJobEntity> submittedJobs = myBulkExportJobDao.findByJobId(jobUuid);
if (submittedJobs.isPresent()) {
BulkExportJobEntity jobEntity = submittedJobs.get();
jobEntity.setStatus(BulkJobStatusEnum.ERROR);
jobEntity.setStatusMessage(e.getMessage());
myBulkExportJobDao.save(jobEntity);
}
return null;
});
}
}
/**
* This method is called by the scheduler to run a pass of the
* generator
*/
@Transactional(value = Transactional.TxType.NEVER)
@Override
public void purgeExpiredFiles() {
Optional<BulkExportJobEntity> jobToDelete = myTxTemplate.execute(t -> {
Pageable page = PageRequest.of(0, 1);
Slice<BulkExportJobEntity> submittedJobs = myBulkExportJobDao.findByExpiry(page, new Date());
if (submittedJobs.isEmpty()) {
return Optional.empty();
}
return Optional.of(submittedJobs.getContent().get(0));
});
if (jobToDelete.isPresent()) {
ourLog.info("Deleting bulk export job: {}", jobToDelete.get().getJobId());
myTxTemplate.execute(t -> {
BulkExportJobEntity job = myBulkExportJobDao.getOne(jobToDelete.get().getId());
for (BulkExportCollectionEntity nextCollection : job.getCollections()) {
for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) {
ourLog.info("Purging bulk data file: {}", nextFile.getResourceId());
getBinaryDao().delete(toId(nextFile.getResourceId()));
getBinaryDao().forceExpungeInExistingTransaction(toId(nextFile.getResourceId()), new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), null);
myBulkExportCollectionFileDao.delete(nextFile);
}
myBulkExportCollectionDao.delete(nextCollection);
}
myBulkExportJobDao.delete(job);
return null;
});
}
}
private void processJob(String theJobUuid) {
Optional<BulkExportJobEntity> jobOpt = myBulkExportJobDao.findByJobId(theJobUuid);
if (!jobOpt.isPresent()) {
ourLog.info("Job appears to be deleted");
return;
}
StopWatch jobStopwatch = new StopWatch();
AtomicInteger jobResourceCounter = new AtomicInteger();
BulkExportJobEntity job = jobOpt.get();
ourLog.info("Bulk export starting generation for batch export job: {}", job);
for (BulkExportCollectionEntity nextCollection : job.getCollections()) {
String nextType = nextCollection.getResourceType();
IFhirResourceDao dao = myDaoRegistry.getResourceDao(nextType);
ourLog.info("Bulk export assembling export of type {} for job {}", nextType, theJobUuid);
ISearchBuilder sb = dao.newSearchBuilder();
Class<? extends IBaseResource> nextTypeClass = myContext.getResourceDefinition(nextType).getImplementingClass();
sb.setType(nextTypeClass, nextType);
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
if (job.getSince() != null) {
map.setLastUpdated(new DateRangeParam(job.getSince(), null));
}
IResultIterator resultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, theJobUuid), null);
storeResultsToFiles(nextCollection, sb, resultIterator, jobResourceCounter, jobStopwatch);
}
job.setStatus(BulkJobStatusEnum.COMPLETE);
updateExpiry(job);
myBulkExportJobDao.save(job);
ourLog.info("Bulk export completed job in {}: {}", jobStopwatch, job);
}
private void storeResultsToFiles(BulkExportCollectionEntity theExportCollection, ISearchBuilder theSearchBuilder, IResultIterator theResultIterator, AtomicInteger theJobResourceCounter, StopWatch theJobStopwatch) {
try (IResultIterator query = theResultIterator) {
if (!query.hasNext()) {
return;
}
AtomicInteger fileCounter = new AtomicInteger(0);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream, Constants.CHARSET_UTF8);
IParser parser = myContext.newJsonParser().setPrettyPrint(false);
List<Long> pidsSpool = new ArrayList<>();
List<IBaseResource> resourcesSpool = new ArrayList<>();
while (query.hasNext()) {
pidsSpool.add(query.next());
fileCounter.incrementAndGet();
theJobResourceCounter.incrementAndGet();
if (pidsSpool.size() >= 10 || !query.hasNext()) {
theSearchBuilder.loadResourcesByPid(pidsSpool, Collections.emptyList(), resourcesSpool, false, null);
for (IBaseResource nextFileResource : resourcesSpool) {
parser.encodeResourceToWriter(nextFileResource, writer);
writer.append("\n");
}
pidsSpool.clear();
resourcesSpool.clear();
if (outputStream.size() >= myFileMaxChars || !query.hasNext()) {
Optional<IIdType> createdId = flushToFiles(theExportCollection, fileCounter, outputStream);
createdId.ifPresent(theIIdType -> ourLog.info("Created resource {} for bulk export file containing {} resources of type {} - Total {} resources ({}/sec)", theIIdType.toUnqualifiedVersionless().getValue(), fileCounter.get(), theExportCollection.getResourceType(), theJobResourceCounter.get(), theJobStopwatch.formatThroughput(theJobResourceCounter.get(), TimeUnit.SECONDS)));
fileCounter.set(0);
}
}
}
} catch (IOException e) {
throw new InternalErrorException(e);
}
}
private Optional<IIdType> flushToFiles(BulkExportCollectionEntity theCollection, AtomicInteger theCounter, ByteArrayOutputStream theOutputStream) {
if (theOutputStream.size() > 0) {
IBaseBinary binary = BinaryUtil.newBinary(myContext);
binary.setContentType(Constants.CT_FHIR_NDJSON);
binary.setContent(theOutputStream.toByteArray());
IIdType createdId = getBinaryDao().create(binary).getResource().getIdElement();
BulkExportCollectionFileEntity file = new BulkExportCollectionFileEntity();
theCollection.getFiles().add(file);
file.setCollection(theCollection);
file.setResource(createdId.getIdPart());
myBulkExportCollectionFileDao.saveAndFlush(file);
theOutputStream.reset();
return Optional.of(createdId);
}
return Optional.empty();
}
@SuppressWarnings("unchecked")
private IFhirResourceDao<IBaseBinary> getBinaryDao() {
return myDaoRegistry.getResourceDao("Binary");
}
@PostConstruct
public void start() {
ourLog.info("Bulk export service starting with refresh interval {}", StopWatch.formatMillis(REFRESH_INTERVAL));
myTxTemplate = new TransactionTemplate(myTxManager);
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(BulkDataExportSvcImpl.class.getName());
jobDetail.setJobClass(BulkDataExportSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(REFRESH_INTERVAL, true, jobDetail);
}
@Transactional
@Override
public JobInfo submitJob(String theOutputFormat, Set<String> theResourceTypes, Date theSince, Set<String> theFilters) {
String outputFormat = Constants.CT_FHIR_NDJSON;
if (isNotBlank(theOutputFormat)) {
outputFormat = theOutputFormat;
}
if (!Constants.CTS_NDJSON.contains(outputFormat)) {
throw new InvalidRequestException("Invalid output format: " + theOutputFormat);
}
StringBuilder requestBuilder = new StringBuilder();
requestBuilder.append("/").append(JpaConstants.OPERATION_EXPORT);
requestBuilder.append("?").append(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT).append("=").append(escapeUrlParam(outputFormat));
Set<String> resourceTypes = theResourceTypes;
if (resourceTypes != null) {
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_TYPE).append("=").append(String.join(",", resourceTypes));
}
Date since = theSince;
if (since != null) {
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_SINCE).append("=").append(new InstantType(since).setTimeZoneZulu(true).getValueAsString());
}
if (theFilters != null && theFilters.size() > 0) {
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_TYPE).append("=").append(String.join(",", theFilters));
}
String request = requestBuilder.toString();
Date cutoff = DateUtils.addMilliseconds(new Date(), -myReuseBulkExportForMillis);
Pageable page = PageRequest.of(0, 10);
Slice<BulkExportJobEntity> existing = myBulkExportJobDao.findExistingJob(page, request, cutoff, BulkJobStatusEnum.ERROR);
if (existing.isEmpty() == false) {
return toSubmittedJobInfo(existing.iterator().next());
}
if (theResourceTypes == null || resourceTypes.isEmpty()) {
// This is probably not a useful default, but having the default be "download the whole
// server" seems like a risky default too. We'll deal with that by having the default involve
// only returning a small time span
resourceTypes = myContext.getResourceNames();
if (since == null) {
since = DateUtils.addDays(new Date(), -1);
}
}
BulkExportJobEntity job = new BulkExportJobEntity();
job.setJobId(UUID.randomUUID().toString());
job.setStatus(BulkJobStatusEnum.SUBMITTED);
job.setSince(since);
job.setCreated(new Date());
job.setRequest(request);
updateExpiry(job);
myBulkExportJobDao.save(job);
for (String nextType : resourceTypes) {
if (!myDaoRegistry.isResourceTypeSupported(nextType)) {
throw new InvalidRequestException("Unknown or unsupported resource type: " + nextType);
}
BulkExportCollectionEntity collection = new BulkExportCollectionEntity();
collection.setJob(job);
collection.setResourceType(nextType);
job.getCollections().add(collection);
myBulkExportCollectionDao.save(collection);
}
ourLog.info("Bulk export job submitted: {}", job.toString());
return toSubmittedJobInfo(job);
}
private JobInfo toSubmittedJobInfo(BulkExportJobEntity theJob) {
return new JobInfo().setJobId(theJob.getJobId());
}
private void updateExpiry(BulkExportJobEntity theJob) {
theJob.setExpiry(DateUtils.addMilliseconds(new Date(), myRetentionPeriod));
}
@Transactional
@Override
public JobInfo getJobStatusOrThrowResourceNotFound(String theJobId) {
BulkExportJobEntity job = myBulkExportJobDao
.findByJobId(theJobId)
.orElseThrow(() -> new ResourceNotFoundException(theJobId));
JobInfo retVal = new JobInfo();
retVal.setJobId(theJobId);
retVal.setStatus(job.getStatus());
retVal.setStatus(job.getStatus());
retVal.setStatusTime(job.getStatusTime());
retVal.setStatusMessage(job.getStatusMessage());
retVal.setRequest(job.getRequest());
if (job.getStatus() == BulkJobStatusEnum.COMPLETE) {
for (BulkExportCollectionEntity nextCollection : job.getCollections()) {
for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) {
retVal.addFile()
.setResourceType(nextCollection.getResourceType())
.setResourceId(toQualifiedBinaryId(nextFile.getResourceId()));
}
}
}
return retVal;
}
private IIdType toId(String theResourceId) {
IIdType retVal = myContext.getVersion().newIdType();
retVal.setValue(theResourceId);
return retVal;
}
private IIdType toQualifiedBinaryId(String theIdPart) {
IIdType retVal = myContext.getVersion().newIdType();
retVal.setParts(null, "Binary", theIdPart, null);
return retVal;
}
@Override
@Transactional
public synchronized void cancelAndPurgeAllJobs() {
myBulkExportCollectionFileDao.deleteAll();
myBulkExportCollectionDao.deleteAll();
myBulkExportJobDao.deleteAll();
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class SubmitJob extends FireAtIntervalJob {
@Autowired
private IBulkDataExportSvc myTarget;
public SubmitJob() {
super(REFRESH_INTERVAL);
}
@Override
protected void doExecute(JobExecutionContext theContext) {
myTarget.buildExportFiles();
}
}
}

View File

@ -0,0 +1,110 @@
package ca.uhn.fhir.jpa.bulk;
import ca.uhn.fhir.jpa.util.JsonDateDeserializer;
import ca.uhn.fhir.jpa.util.JsonDateSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
public class BulkExportResponseJson {
@JsonProperty("transactionTime")
@JsonSerialize(using = JsonDateSerializer.class)
@JsonDeserialize(using = JsonDateDeserializer.class)
private Date myTransactionTime;
@JsonProperty("request")
private String myRequest;
@JsonProperty("requiresAccessToken")
private Boolean myRequiresAccessToken;
@JsonProperty("output")
private List<Output> myOutput;
@JsonProperty("error")
private List<Output> myError;
public Date getTransactionTime() {
return myTransactionTime;
}
public BulkExportResponseJson setTransactionTime(Date theTransactionTime) {
myTransactionTime = theTransactionTime;
return this;
}
public String getRequest() {
return myRequest;
}
public BulkExportResponseJson setRequest(String theRequest) {
myRequest = theRequest;
return this;
}
public Boolean getRequiresAccessToken() {
return myRequiresAccessToken;
}
public BulkExportResponseJson setRequiresAccessToken(Boolean theRequiresAccessToken) {
myRequiresAccessToken = theRequiresAccessToken;
return this;
}
public List<Output> getOutput() {
if (myOutput == null) {
myOutput = new ArrayList<>();
}
return myOutput;
}
public List<Output> getError() {
if (myError == null) {
myError = new ArrayList<>();
}
return myError;
}
public Output addOutput() {
Output retVal = new Output();
getOutput().add(retVal);
return retVal;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
public static class Output {
@JsonProperty("type")
private String myType;
@JsonProperty("url")
private String myUrl;
public String getType() {
return myType;
}
public Output setType(String theType) {
myType = theType;
return this;
}
public String getUrl() {
return myUrl;
}
public Output setUrl(String theUrl) {
myUrl = theUrl;
return this;
}
}
}

View File

@ -0,0 +1,10 @@
package ca.uhn.fhir.jpa.bulk;
public enum BulkJobStatusEnum {
SUBMITTED,
BUILDING,
COMPLETE,
ERROR
}

View File

@ -0,0 +1,114 @@
package ca.uhn.fhir.jpa.bulk;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
public interface IBulkDataExportSvc {
void buildExportFiles();
@Transactional(value = Transactional.TxType.NEVER)
void purgeExpiredFiles();
JobInfo submitJob(String theOutputFormat, Set<String> theResourceTypes, Date theSince, Set<String> theFilters);
JobInfo getJobStatusOrThrowResourceNotFound(String theJobId);
void cancelAndPurgeAllJobs();
class JobInfo {
private String myJobId;
private BulkJobStatusEnum myStatus;
private List<FileEntry> myFiles;
private String myRequest;
private Date myStatusTime;
private String myStatusMessage;
public String getRequest() {
return myRequest;
}
public void setRequest(String theRequest) {
myRequest = theRequest;
}
public Date getStatusTime() {
return myStatusTime;
}
public JobInfo setStatusTime(Date theStatusTime) {
myStatusTime = theStatusTime;
return this;
}
public String getJobId() {
return myJobId;
}
public JobInfo setJobId(String theJobId) {
myJobId = theJobId;
return this;
}
public List<FileEntry> getFiles() {
if (myFiles == null) {
myFiles = new ArrayList<>();
}
return myFiles;
}
public BulkJobStatusEnum getStatus() {
return myStatus;
}
public JobInfo setStatus(BulkJobStatusEnum theStatus) {
myStatus = theStatus;
return this;
}
public String getStatusMessage() {
return myStatusMessage;
}
public JobInfo setStatusMessage(String theStatusMessage) {
myStatusMessage = theStatusMessage;
return this;
}
public FileEntry addFile() {
FileEntry retVal = new FileEntry();
getFiles().add(retVal);
return retVal;
}
}
class FileEntry {
private String myResourceType;
private IIdType myResourceId;
public String getResourceType() {
return myResourceType;
}
public FileEntry setResourceType(String theResourceType) {
myResourceType = theResourceType;
return this;
}
public IIdType getResourceId() {
return myResourceId;
}
public FileEntry setResourceId(IIdType theResourceId) {
myResourceId = theResourceId;
return this;
}
}
}

View File

@ -6,11 +6,17 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl;
import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory;
import ca.uhn.fhir.jpa.sched.SchedulerServiceImpl;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
@ -29,7 +35,6 @@ import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -38,15 +43,10 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.scheduling.TaskScheduler; 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.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import javax.annotation.Nonnull;
/* /*
* #%L * #%L
* HAPI FHIR JPA Server * HAPI FHIR JPA Server
@ -69,7 +69,6 @@ import javax.annotation.Nonnull;
@Configuration @Configuration
@EnableScheduling
@EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data")
@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters = { @ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BaseConfig.class), @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BaseConfig.class),
@ -77,8 +76,7 @@ import javax.annotation.Nonnull;
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*\\.test\\..*"), @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*\\.test\\..*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"), @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.module.standalone.*")}) @ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.module.standalone.*")})
public abstract class BaseConfig {
public abstract class BaseConfig implements SchedulingConfigurer {
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor"; public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider"; public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider";
@ -86,18 +84,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Autowired @Autowired
protected Environment myEnv; protected Environment myEnv;
@Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) {
theTaskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean("myDaoRegistry") @Bean("myDaoRegistry")
public DaoRegistry daoRegistry() { public DaoRegistry daoRegistry() {
return new DaoRegistry(); return new DaoRegistry();
} }
@Bean(autowire = Autowire.BY_TYPE) @Bean
public DatabaseBackedPagingProvider databaseBackedPagingProvider() { public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
return new DatabaseBackedPagingProvider(); return new DatabaseBackedPagingProvider();
} }
@ -226,7 +218,7 @@ public abstract class BaseConfig implements SchedulingConfigurer {
* Subclasses may override * Subclasses may override
*/ */
protected boolean isSupported(String theResourceType) { protected boolean isSupported(String theResourceType) {
return daoRegistry().getResourceDaoIfExists(theResourceType) != null; return daoRegistry().getResourceDaoOrNull(theResourceType) != null;
} }
@Bean @Bean
@ -241,6 +233,30 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return retVal; return retVal;
} }
@Bean
public ISchedulerService schedulerService() {
return new SchedulerServiceImpl();
}
@Bean
public AutowiringSpringBeanJobFactory schedulerJobFactory() {
return new AutowiringSpringBeanJobFactory();
}
@Bean
@Lazy
public IBulkDataExportSvc bulkDataExportSvc() {
return new BulkDataExportSvcImpl();
}
@Bean
@Lazy
public BulkDataExportProvider bulkDataExportProvider() {
return new BulkDataExportProvider();
}
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) { public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer())); theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity"); theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");

View File

@ -80,7 +80,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> { public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
@Autowired @Autowired
protected PlatformTransactionManager myPlatformTransactionManager; protected PlatformTransactionManager myPlatformTransactionManager;
@ -551,10 +551,22 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
myEntityManager.merge(entity); myEntityManager.merge(entity);
} }
private void validateExpungeEnabled() {
if (!myDaoConfig.isExpungeEnabled()) {
throw new MethodNotAllowedException("$expunge is not enabled on this server");
}
}
@Override @Override
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
validateExpungeEnabled();
return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest);
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
BaseHasResource entity = txTemplate.execute(t -> readEntity(theId, theRequest)); BaseHasResource entity = txTemplate.execute(t -> readEntity(theId, theRequest));

View File

@ -41,6 +41,9 @@ public class DaoRegistry implements ApplicationContextAware, IDaoRegistry {
@Autowired @Autowired
private FhirContext myContext; private FhirContext myContext;
private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao;
private volatile IFhirSystemDao<?, ?> mySystemDao;
private Set<String> mySupportedResourceTypes;
/** /**
* Constructor * Constructor
@ -49,11 +52,6 @@ public class DaoRegistry implements ApplicationContextAware, IDaoRegistry {
super(); super();
} }
private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao;
private volatile IFhirSystemDao<?, ?> mySystemDao;
private Set<String> mySupportedResourceTypes;
public void setSupportedResourceTypes(Collection<String> theSupportedResourceTypes) { public void setSupportedResourceTypes(Collection<String> theSupportedResourceTypes) {
HashSet<String> supportedResourceTypes = new HashSet<>(); HashSet<String> supportedResourceTypes = new HashSet<>();
if (theSupportedResourceTypes != null) { if (theSupportedResourceTypes != null) {
@ -138,7 +136,10 @@ public class DaoRegistry implements ApplicationContextAware, IDaoRegistry {
@Override @Override
public boolean isResourceTypeSupported(String theResourceType) { public boolean isResourceTypeSupported(String theResourceType) {
return mySupportedResourceTypes == null || mySupportedResourceTypes.contains(theResourceType); if (mySupportedResourceTypes == null) {
return getResourceDaoOrNull(theResourceType) != null;
}
return mySupportedResourceTypes.contains(theResourceType);
} }
private void init() { private void init() {

View File

@ -116,6 +116,8 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
ExpungeOutcome expunge(IIdType theIIdType, ExpungeOptions theExpungeOptions, RequestDetails theRequest); ExpungeOutcome expunge(IIdType theIIdType, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
Class<T> getResourceType(); Class<T> getResourceType();
IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails); IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails);

View File

@ -185,9 +185,9 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
if (theRequestDetails != null) { if (theRequestDetails != null) {
if (outcome.getResource() != null) { if (outcome.getResource() != null) {
String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(null, prefer); PreferHeader.PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(null, prefer).getReturn();
if (preferReturn != null) { if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) { if (preferReturn == PreferHeader.PreferReturnEnum.REPRESENTATION) {
outcome.fireResourceViewCallbacks(); outcome.fireResourceViewCallbacks();
myVersionAdapter.setResource(newEntry, outcome.getResource()); myVersionAdapter.setResource(newEntry, outcome.getResource());
} }
@ -211,7 +211,10 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
String nextReplacementIdPart = nextReplacementId.getValueAsString(); String nextReplacementIdPart = nextReplacementId.getValueAsString();
if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) { if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) {
matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart); matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart); String escapedUrlParam = UrlUtil.escapeUrlParam(nextTemporaryIdPart);
if (isNotBlank(escapedUrlParam)) {
matchUrl = matchUrl.replace(escapedUrlParam, nextReplacementIdPart);
}
} }
} }
} }

View File

@ -0,0 +1,34 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity;
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IBulkExportCollectionDao extends JpaRepository<BulkExportCollectionEntity, Long> {
// nothing currently
}

View File

@ -0,0 +1,28 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
import org.springframework.data.jpa.repository.JpaRepository;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IBulkExportCollectionFileDao extends JpaRepository<BulkExportCollectionFileEntity, Long> {
// nothing currently
}

View File

@ -0,0 +1,47 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.bulk.BulkJobStatusEnum;
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Date;
import java.util.Optional;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IBulkExportJobDao extends JpaRepository<BulkExportJobEntity, Long> {
@Query("SELECT j FROM BulkExportJobEntity j WHERE j.myJobId = :jobid")
Optional<BulkExportJobEntity> findByJobId(@Param("jobid") String theUuid);
@Query("SELECT j FROM BulkExportJobEntity j WHERE j.myStatus = :status")
Slice<BulkExportJobEntity> findByStatus(Pageable thePage, @Param("status") BulkJobStatusEnum theSubmitted);
@Query("SELECT j FROM BulkExportJobEntity j WHERE j.myExpiry < :cutoff")
Slice<BulkExportJobEntity> findByExpiry(Pageable thePage, @Param("cutoff") Date theCutoff);
@Query("SELECT j FROM BulkExportJobEntity j WHERE j.myRequest = :request AND j.myCreated > :createdAfter AND j.myStatus <> :status")
Slice<BulkExportJobEntity> findExistingJob(Pageable thePage, @Param("request") String theRequest, @Param("createdAfter") Date theCreatedAfter, @Param("status") BulkJobStatusEnum theNotStatus);
}

View File

@ -37,11 +37,11 @@ public interface ISearchDao extends JpaRepository<Search, Long> {
@Query("SELECT s FROM Search s LEFT OUTER JOIN FETCH s.myIncludes WHERE s.myUuid = :uuid") @Query("SELECT s FROM Search s LEFT OUTER JOIN FETCH s.myIncludes WHERE s.myUuid = :uuid")
Optional<Search> findByUuidAndFetchIncludes(@Param("uuid") String theUuid); Optional<Search> findByUuidAndFetchIncludes(@Param("uuid") String theUuid);
@Query("SELECT s.myId FROM Search s WHERE s.mySearchLastReturned < :cutoff") @Query("SELECT s.myId FROM Search s WHERE (s.mySearchLastReturned < :cutoff) AND (s.myExpiryOrNull IS NULL OR s.myExpiryOrNull < :now)")
Slice<Long> findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, Pageable thePage); Slice<Long> findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, @Param("now") Date theNow, Pageable thePage);
@Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND s.myCreated > :cutoff AND s.myDeleted = false") @Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND (s.myCreated > :cutoff) AND s.myDeleted = false")
Collection<Search> find(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff); Collection<Search> findWithCutoffOrExpiry(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff);
@Modifying @Modifying
@Query("UPDATE Search s SET s.mySearchLastReturned = :last WHERE s.myId = :pid") @Query("UPDATE Search s SET s.mySearchLastReturned = :last WHERE s.myId = :pid")

View File

@ -49,10 +49,6 @@ public abstract class ExpungeService {
public ExpungeOutcome expunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { public ExpungeOutcome expunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions); ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions);
if (!myConfig.isExpungeEnabled()) {
throw new MethodNotAllowedException("$expunge is not enabled on this server");
}
if (theExpungeOptions.getLimit() < 1) { if (theExpungeOptions.getLimit() < 1) {
throw new InvalidRequestException("Expunge limit may not be less than 1. Received expunge limit " + theExpungeOptions.getLimit() + "."); throw new InvalidRequestException("Expunge limit may not be less than 1. Received expunge limit " + theExpungeOptions.getLimit() + ".");
} }

View File

@ -55,6 +55,16 @@ public class PartitionRunner {
return; return;
} }
if (callableTasks.size() == 1) {
try {
callableTasks.get(0).call();
return;
} catch (Exception e) {
ourLog.error("Error while expunging.", e);
throw new InternalErrorException(e);
}
}
ExecutorService executorService = buildExecutor(callableTasks.size()); ExecutorService executorService = buildExecutor(callableTasks.size());
try { try {
List<Future<Void>> futures = executorService.invokeAll(callableTasks); List<Future<Void>> futures = executorService.invokeAll(callableTasks);

View File

@ -0,0 +1,65 @@
package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@Entity
@Table(name = "HFJ_BLK_EXPORT_COLLECTION")
public class BulkExportCollectionEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKEXCOL_PID")
@SequenceGenerator(name = "SEQ_BLKEXCOL_PID", sequenceName = "SEQ_BLKEXCOL_PID")
@Column(name = "PID")
private Long myId;
@ManyToOne
@JoinColumn(name = "JOB_PID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name="FK_BLKEXCOL_JOB"))
private BulkExportJobEntity myJob;
@Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false)
private String myResourceType;
@Column(name = "TYPE_FILTER", length = 1000, nullable = true)
private String myFilter;
@Version
@Column(name = "OPTLOCK", nullable = false)
private int myVersion;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "myCollection")
private Collection<BulkExportCollectionFileEntity> myFiles;
public void setJob(BulkExportJobEntity theJob) {
myJob = theJob;
}
public String getResourceType() {
return myResourceType;
}
public void setResourceType(String theResourceType) {
myResourceType = theResourceType;
}
public String getFilter() {
return myFilter;
}
public void setFilter(String theFilter) {
myFilter = theFilter;
}
public int getVersion() {
return myVersion;
}
public void setVersion(int theVersion) {
myVersion = theVersion;
}
public Collection<BulkExportCollectionFileEntity> getFiles() {
if (myFiles == null) {
myFiles = new ArrayList<>();
}
return myFiles;
}
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import javax.persistence.*;
@Entity
@Table(name = "HFJ_BLK_EXPORT_COLFILE")
public class BulkExportCollectionFileEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKEXCOLFILE_PID")
@SequenceGenerator(name = "SEQ_BLKEXCOLFILE_PID", sequenceName = "SEQ_BLKEXCOLFILE_PID")
@Column(name = "PID")
private Long myId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "COLLECTION_PID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name="FK_BLKEXCOLFILE_COLLECT"))
private BulkExportCollectionEntity myCollection;
@Column(name = "RES_ID", length = ForcedId.MAX_FORCED_ID_LENGTH, nullable = false)
private String myResourceId;
public void setCollection(BulkExportCollectionEntity theCollection) {
myCollection = theCollection;
}
public void setResource(String theResourceId) {
myResourceId = theResourceId;
}
public String getResourceId() {
return myResourceId;
}
}

View File

@ -0,0 +1,157 @@
package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.jpa.bulk.BulkJobStatusEnum;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.r5.model.InstantType;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Entity
@Table(name = "HFJ_BLK_EXPORT_JOB", uniqueConstraints = {
@UniqueConstraint(name = "IDX_BLKEX_JOB_ID", columnNames = "JOB_ID")
}, indexes = {
@Index(name = "IDX_BLKEX_EXPTIME", columnList = "EXP_TIME")
})
public class BulkExportJobEntity {
public static final int REQUEST_LENGTH = 500;
public static final int STATUS_MESSAGE_LEN = 500;
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKEXJOB_PID")
@SequenceGenerator(name = "SEQ_BLKEXJOB_PID", sequenceName = "SEQ_BLKEXJOB_PID")
@Column(name = "PID")
private Long myId;
@Column(name = "JOB_ID", length = Search.UUID_COLUMN_LENGTH, nullable = false)
private String myJobId;
@Enumerated(EnumType.STRING)
@Column(name = "JOB_STATUS", length = 10, nullable = false)
private BulkJobStatusEnum myStatus;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED_TIME", nullable = false)
private Date myCreated;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "STATUS_TIME", nullable = false)
private Date myStatusTime;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "EXP_TIME", nullable = false)
private Date myExpiry;
@Column(name = "REQUEST", nullable = false, length = REQUEST_LENGTH)
private String myRequest;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "myJob")
private Collection<BulkExportCollectionEntity> myCollections;
@Version
@Column(name = "OPTLOCK", nullable = false)
private int myVersion;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "EXP_SINCE", nullable = true)
private Date mySince;
@Column(name = "STATUS_MESSAGE", nullable = true, length = STATUS_MESSAGE_LEN)
private String myStatusMessage;
public Date getCreated() {
return myCreated;
}
public void setCreated(Date theCreated) {
myCreated = theCreated;
}
public String getStatusMessage() {
return myStatusMessage;
}
public void setStatusMessage(String theStatusMessage) {
myStatusMessage = theStatusMessage;
}
public String getRequest() {
return myRequest;
}
public void setRequest(String theRequest) {
myRequest = theRequest;
}
public void setExpiry(Date theExpiry) {
myExpiry = theExpiry;
}
public Collection<BulkExportCollectionEntity> getCollections() {
if (myCollections == null) {
myCollections = new ArrayList<>();
}
return myCollections;
}
public String getJobId() {
return myJobId;
}
public void setJobId(String theJobId) {
myJobId = theJobId;
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
if (isNotBlank(myJobId)) {
b.append("jobId", myJobId);
}
if (myStatus != null) {
b.append("status", myStatus + " " + new InstantType(myStatusTime).getValueAsString());
}
b.append("created", new InstantType(myExpiry).getValueAsString());
b.append("expiry", new InstantType(myExpiry).getValueAsString());
b.append("request", myRequest);
b.append("since", mySince);
if (isNotBlank(myStatusMessage)) {
b.append("statusMessage", myStatusMessage);
}
return b.toString();
}
public BulkJobStatusEnum getStatus() {
return myStatus;
}
public void setStatus(BulkJobStatusEnum theStatus) {
if (myStatus != theStatus) {
myStatusTime = new Date();
myStatus = theStatus;
}
}
public Date getStatusTime() {
return myStatusTime;
}
public int getVersion() {
return myVersion;
}
public void setVersion(int theVersion) {
myVersion = theVersion;
}
public Long getId() {
return myId;
}
public Date getSince() {
if (mySince != null) {
return new Date(mySince.getTime());
}
return null;
}
public void setSince(Date theSince) {
mySince = theSince;
}
}

View File

@ -59,6 +59,9 @@ public class Search implements ICachedSearchDetails, Serializable {
private Integer myFailureCode; private Integer myFailureCode;
@Column(name = "FAILURE_MESSAGE", length = FAILURE_MESSAGE_LENGTH, nullable = true) @Column(name = "FAILURE_MESSAGE", length = FAILURE_MESSAGE_LENGTH, nullable = true)
private String myFailureMessage; private String myFailureMessage;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "EXPIRY_OR_NULL", nullable = true)
private Date myExpiryOrNull;
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCH") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCH")
@SequenceGenerator(name = "SEQ_SEARCH", sequenceName = "SEQ_SEARCH") @SequenceGenerator(name = "SEQ_SEARCH", sequenceName = "SEQ_SEARCH")
@ -108,7 +111,6 @@ public class Search implements ICachedSearchDetails, Serializable {
@Lob @Lob
@Column(name = "SEARCH_PARAM_MAP", nullable = true) @Column(name = "SEARCH_PARAM_MAP", nullable = true)
private byte[] mySearchParameterMap; private byte[] mySearchParameterMap;
/** /**
* Constructor * Constructor
*/ */
@ -116,6 +118,14 @@ public class Search implements ICachedSearchDetails, Serializable {
super(); super();
} }
public Date getExpiryOrNull() {
return myExpiryOrNull;
}
public void setExpiryOrNull(Date theExpiryOrNull) {
myExpiryOrNull = theExpiryOrNull;
}
public Boolean getDeleted() { public Boolean getDeleted() {
return myDeleted; return myDeleted;
} }
@ -230,11 +240,15 @@ public class Search implements ICachedSearchDetails, Serializable {
} }
public void setSearchQueryString(String theSearchQueryString) { public void setSearchQueryString(String theSearchQueryString) {
if (theSearchQueryString != null && theSearchQueryString.length() > MAX_SEARCH_QUERY_STRING) { if (theSearchQueryString == null || theSearchQueryString.length() > MAX_SEARCH_QUERY_STRING) {
mySearchQueryString = null; // We want this field to always have a wide distribution of values in order
// to avoid optimizers avoiding using it if it has lots of nulls, so in the
// case of null, just put a value that will never be hit
mySearchQueryString = UUID.randomUUID().toString();
} else { } else {
mySearchQueryString = theSearchQueryString; mySearchQueryString = theSearchQueryString;
} }
mySearchQueryStringHash = mySearchQueryString.hashCode();
} }
public SearchTypeEnum getSearchType() { public SearchTypeEnum getSearchType() {

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.sched;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory myBeanFactory;
private ApplicationContext myAppCtx;
@Override
public void setApplicationContext(final ApplicationContext theApplicationContext) {
myAppCtx = theApplicationContext;
myBeanFactory = theApplicationContext.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
Object job = super.createJobInstance(bundle);
myBeanFactory.autowireBean(job);
if (job instanceof ApplicationContextAware) {
((ApplicationContextAware) job).setApplicationContext(myAppCtx);
}
return job;
}
}

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.jpa.sched;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.annotation.PostConstruct;
public class QuartzTableSeeder {
@Autowired
private LocalContainerEntityManagerFactoryBean myEntityManagerFactory;
@PostConstruct
public void start() {
}
}

View File

@ -0,0 +1,559 @@
package ca.uhn.fhir.jpa.sched;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.Validate;
import org.quartz.Calendar;
import org.quartz.*;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.spi.JobFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static org.quartz.impl.StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME;
/**
* This class provides task scheduling for the entire module using the Quartz library.
* Inside here, we have two schedulers:
* <ul>
* <li>
* The <b>Local Scheduler</b> handles tasks that need to execute locally. This
* typically means things that should happen on all nodes in a clustered
* environment.
* </li>
* <li>
* The <b>Cluster Scheduler</b> handles tasks that are distributed and should be
* handled by only one node in the cluster (assuming a clustered server). If the
* server is not clustered, this scheduler acts the same way as the
* local scheduler.
* </li>
* </ul>
*/
public class SchedulerServiceImpl implements ISchedulerService {
public static final String SCHEDULING_DISABLED = "scheduling_disabled";
public static final String SCHEDULING_DISABLED_EQUALS_TRUE = SCHEDULING_DISABLED + "=true";
private static final Logger ourLog = LoggerFactory.getLogger(SchedulerServiceImpl.class);
private static int ourNextSchedulerId = 0;
private Scheduler myLocalScheduler;
private Scheduler myClusteredScheduler;
private String myThreadNamePrefix;
private boolean myLocalSchedulingEnabled;
private boolean myClusteredSchedulingEnabled;
@Autowired
private AutowiringSpringBeanJobFactory mySpringBeanJobFactory;
private AtomicBoolean myStopping = new AtomicBoolean(false);
@Autowired
private Environment myEnvironment;
/**
* Constructor
*/
public SchedulerServiceImpl() {
setThreadNamePrefix("hapi-fhir-jpa-scheduler");
setLocalSchedulingEnabled(true);
setClusteredSchedulingEnabled(true);
}
public boolean isLocalSchedulingEnabled() {
return myLocalSchedulingEnabled;
}
public void setLocalSchedulingEnabled(boolean theLocalSchedulingEnabled) {
myLocalSchedulingEnabled = theLocalSchedulingEnabled;
}
public boolean isClusteredSchedulingEnabled() {
return myClusteredSchedulingEnabled;
}
public void setClusteredSchedulingEnabled(boolean theClusteredSchedulingEnabled) {
myClusteredSchedulingEnabled = theClusteredSchedulingEnabled;
}
public String getThreadNamePrefix() {
return myThreadNamePrefix;
}
public void setThreadNamePrefix(String theThreadNamePrefix) {
myThreadNamePrefix = theThreadNamePrefix;
}
@PostConstruct
public void start() throws SchedulerException {
myLocalScheduler = createLocalScheduler();
myClusteredScheduler = createClusteredScheduler();
myStopping.set(false);
}
/**
* We defer startup of executing started tasks until we're sure we're ready for it
* and the startup is completely done
*/
@EventListener
public void contextStarted(ContextRefreshedEvent theEvent) throws SchedulerException {
try {
ourLog.info("Starting task schedulers for context {}", theEvent != null ? theEvent.getApplicationContext().getId() : "null");
if (myLocalScheduler != null) {
myLocalScheduler.start();
}
if (myClusteredScheduler != null) {
myClusteredScheduler.start();
}
} catch (Exception e) {
ourLog.error("Failed to start context", e);
throw new SchedulerException(e);
}
}
private Scheduler createLocalScheduler() throws SchedulerException {
if (!isLocalSchedulingEnabled() || isSchedulingDisabledForUnitTests()) {
return new NullScheduler();
}
Properties localProperties = new Properties();
localProperties.setProperty(PROP_SCHED_INSTANCE_NAME, "local-" + ourNextSchedulerId++);
quartzPropertiesCommon(localProperties);
quartzPropertiesLocal(localProperties);
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(localProperties);
Scheduler scheduler = factory.getScheduler();
configureSchedulerCommon(scheduler);
scheduler.standby();
return scheduler;
}
private Scheduler createClusteredScheduler() throws SchedulerException {
if (!isClusteredSchedulingEnabled() || isSchedulingDisabledForUnitTests()) {
return new NullScheduler();
}
Properties clusteredProperties = new Properties();
clusteredProperties.setProperty(PROP_SCHED_INSTANCE_NAME, "clustered-" + ourNextSchedulerId++);
quartzPropertiesCommon(clusteredProperties);
quartzPropertiesClustered(clusteredProperties);
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(clusteredProperties);
Scheduler scheduler = factory.getScheduler();
configureSchedulerCommon(scheduler);
scheduler.standby();
return scheduler;
}
private void configureSchedulerCommon(Scheduler theScheduler) throws SchedulerException {
theScheduler.setJobFactory(mySpringBeanJobFactory);
}
@PreDestroy
public void stop() throws SchedulerException {
ourLog.info("Shutting down task scheduler...");
myStopping.set(true);
myLocalScheduler.shutdown(true);
myClusteredScheduler.shutdown(true);
}
@Override
public void purgeAllScheduledJobsForUnitTest() throws SchedulerException {
myLocalScheduler.clear();
myClusteredScheduler.clear();
}
@Override
public void logStatus() {
try {
Set<JobKey> keys = myLocalScheduler.getJobKeys(GroupMatcher.anyGroup());
String keysString = keys.stream().map(t -> t.getName()).collect(Collectors.joining(", "));
ourLog.info("Local scheduler has jobs: {}", keysString);
keys = myClusteredScheduler.getJobKeys(GroupMatcher.anyGroup());
keysString = keys.stream().map(t -> t.getName()).collect(Collectors.joining(", "));
ourLog.info("Clustered scheduler has jobs: {}", keysString);
} catch (SchedulerException e) {
throw new InternalErrorException(e);
}
}
@Override
public void scheduleFixedDelay(long theIntervalMillis, boolean theClusteredTask, ScheduledJobDefinition theJobDefinition) {
Validate.isTrue(theIntervalMillis >= 100);
Validate.notNull(theJobDefinition);
Validate.notNull(theJobDefinition.getJobClass());
Validate.notBlank(theJobDefinition.getId());
JobKey jobKey = new JobKey(theJobDefinition.getId());
JobDetailImpl jobDetail = new NonConcurrentJobDetailImpl();
jobDetail.setJobClass(theJobDefinition.getJobClass());
jobDetail.setKey(jobKey);
jobDetail.setName(theJobDefinition.getId());
jobDetail.setJobDataMap(new JobDataMap(theJobDefinition.getJobData()));
ScheduleBuilder<? extends Trigger> schedule = SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInMilliseconds(theIntervalMillis)
.repeatForever();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.startNow()
.withSchedule(schedule)
.build();
Set<? extends Trigger> triggers = Sets.newHashSet(trigger);
try {
Scheduler scheduler;
if (theClusteredTask) {
scheduler = myClusteredScheduler;
} else {
scheduler = myLocalScheduler;
}
scheduler.scheduleJob(jobDetail, triggers, true);
} catch (SchedulerException e) {
ourLog.error("Failed to schedule job", e);
throw new InternalErrorException(e);
}
}
@Override
public boolean isStopping() {
return myStopping.get();
}
/**
* Properties for the local scheduler (see the class docs to learn what this means)
*/
protected void quartzPropertiesLocal(Properties theProperties) {
// nothing
}
/**
* Properties for the cluster scheduler (see the class docs to learn what this means)
*/
protected void quartzPropertiesClustered(Properties theProperties) {
// theProperties.put("org.quartz.jobStore.tablePrefix", "QRTZHFJC_");
}
protected void quartzPropertiesCommon(Properties theProperties) {
theProperties.put("org.quartz.threadPool.threadCount", "4");
theProperties.put("org.quartz.threadPool.threadNamePrefix", getThreadNamePrefix() + "-" + theProperties.get(PROP_SCHED_INSTANCE_NAME));
}
private boolean isSchedulingDisabledForUnitTests() {
String schedulingDisabled = myEnvironment.getProperty(SCHEDULING_DISABLED);
return "true".equals(schedulingDisabled);
}
private static class NonConcurrentJobDetailImpl extends JobDetailImpl {
private static final long serialVersionUID = 5716197221121989740L;
// All HAPI FHIR jobs shouldn't allow concurrent execution
@Override
public boolean isConcurrentExectionDisallowed() {
return true;
}
}
private static class NullScheduler implements Scheduler {
@Override
public String getSchedulerName() {
return null;
}
@Override
public String getSchedulerInstanceId() {
return null;
}
@Override
public SchedulerContext getContext() {
return null;
}
@Override
public void start() {
}
@Override
public void startDelayed(int seconds) {
}
@Override
public boolean isStarted() {
return false;
}
@Override
public void standby() {
}
@Override
public boolean isInStandbyMode() {
return false;
}
@Override
public void shutdown() {
}
@Override
public void shutdown(boolean waitForJobsToComplete) {
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public SchedulerMetaData getMetaData() {
return null;
}
@Override
public List<JobExecutionContext> getCurrentlyExecutingJobs() {
return null;
}
@Override
public void setJobFactory(JobFactory factory) {
}
@Override
public ListenerManager getListenerManager() {
return null;
}
@Override
public Date scheduleJob(JobDetail jobDetail, Trigger trigger) {
return null;
}
@Override
public Date scheduleJob(Trigger trigger) {
return null;
}
@Override
public void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, boolean replace) {
}
@Override
public void scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) {
}
@Override
public boolean unscheduleJob(TriggerKey triggerKey) {
return false;
}
@Override
public boolean unscheduleJobs(List<TriggerKey> triggerKeys) {
return false;
}
@Override
public Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger) {
return null;
}
@Override
public void addJob(JobDetail jobDetail, boolean replace) {
}
@Override
public void addJob(JobDetail jobDetail, boolean replace, boolean storeNonDurableWhileAwaitingScheduling) {
}
@Override
public boolean deleteJob(JobKey jobKey) {
return false;
}
@Override
public boolean deleteJobs(List<JobKey> jobKeys) {
return false;
}
@Override
public void triggerJob(JobKey jobKey) {
}
@Override
public void triggerJob(JobKey jobKey, JobDataMap data) {
}
@Override
public void pauseJob(JobKey jobKey) {
}
@Override
public void pauseJobs(GroupMatcher<JobKey> matcher) {
}
@Override
public void pauseTrigger(TriggerKey triggerKey) {
}
@Override
public void pauseTriggers(GroupMatcher<TriggerKey> matcher) {
}
@Override
public void resumeJob(JobKey jobKey) {
}
@Override
public void resumeJobs(GroupMatcher<JobKey> matcher) {
}
@Override
public void resumeTrigger(TriggerKey triggerKey) {
}
@Override
public void resumeTriggers(GroupMatcher<TriggerKey> matcher) {
}
@Override
public void pauseAll() {
}
@Override
public void resumeAll() {
}
@Override
public List<String> getJobGroupNames() {
return null;
}
@Override
public Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher) {
return null;
}
@Override
public List<? extends Trigger> getTriggersOfJob(JobKey jobKey) {
return null;
}
@Override
public List<String> getTriggerGroupNames() {
return null;
}
@Override
public Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher) {
return null;
}
@Override
public Set<String> getPausedTriggerGroups() {
return null;
}
@Override
public JobDetail getJobDetail(JobKey jobKey) {
return null;
}
@Override
public Trigger getTrigger(TriggerKey triggerKey) {
return null;
}
@Override
public Trigger.TriggerState getTriggerState(TriggerKey triggerKey) {
return null;
}
@Override
public void resetTriggerFromErrorState(TriggerKey triggerKey) {
}
@Override
public void addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers) {
}
@Override
public boolean deleteCalendar(String calName) {
return false;
}
@Override
public Calendar getCalendar(String calName) {
return null;
}
@Override
public List<String> getCalendarNames() {
return null;
}
@Override
public boolean interrupt(JobKey jobKey) throws UnableToInterruptJobException {
return false;
}
@Override
public boolean interrupt(String fireInstanceId) throws UnableToInterruptJobException {
return false;
}
@Override
public boolean checkExists(JobKey jobKey) {
return false;
}
@Override
public boolean checkExists(TriggerKey triggerKey) {
return false;
}
@Override
public void clear() {
}
}
}

View File

@ -21,12 +21,17 @@ package ca.uhn.fhir.jpa.search;
*/ */
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_CUTOFF_SLACK; import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_CUTOFF_SLACK;
/** /**
@ -43,6 +48,8 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Autowired @Autowired
private ISearchCacheSvc mySearchCacheSvc; private ISearchCacheSvc mySearchCacheSvc;
@Autowired
private ISchedulerService mySchedulerService;
@Override @Override
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@ -50,7 +57,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
mySearchCacheSvc.pollForStaleSearchesAndDeleteThem(); mySearchCacheSvc.pollForStaleSearchesAndDeleteThem();
} }
@Scheduled(fixedDelay = DEFAULT_CUTOFF_SLACK) @PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(StaleSearchDeletingSvcImpl.class.getName());
jobDetail.setJobClass(StaleSearchDeletingSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(DEFAULT_CUTOFF_SLACK, true, jobDetail);
}
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public synchronized void schedulePollForStaleSearches() { public synchronized void schedulePollForStaleSearches() {
@ -58,4 +72,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
pollForStaleSearchesAndDeleteThem(); pollForStaleSearchesAndDeleteThem();
} }
} }
public static class SubmitJob implements Job {
@Autowired
private IStaleSearchDeletingSvc myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.schedulePollForStaleSearches();
}
}
} }

View File

@ -0,0 +1,8 @@
package ca.uhn.fhir.jpa.search;
public class WarmSearchDefinition {
private String mySearchUrl;
private long myRefreshPeriodMillis;
}

View File

@ -21,12 +21,16 @@ package ca.uhn.fhir.jpa.search.cache;
*/ */
import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
@ -36,6 +40,8 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
@Autowired @Autowired
private PlatformTransactionManager myTxManager; private PlatformTransactionManager myTxManager;
@Autowired
private ISchedulerService mySchedulerService;
private ConcurrentHashMap<Long, Date> myUnsyncedLastUpdated = new ConcurrentHashMap<>(); private ConcurrentHashMap<Long, Date> myUnsyncedLastUpdated = new ConcurrentHashMap<>();
@ -44,11 +50,18 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
myUnsyncedLastUpdated.put(theSearch.getId(), theDate); myUnsyncedLastUpdated.put(theSearch.getId(), theDate);
} }
@PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(BaseSearchCacheSvcImpl.class.getName());
jobDetail.setJobClass(BaseSearchCacheSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(10 * DateUtils.MILLIS_PER_SECOND, false, jobDetail);
}
@Override @Override
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
public void flushLastUpdated() { public void flushLastUpdated() {
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(t->{ txTemplate.execute(t -> {
for (Iterator<Map.Entry<Long, Date>> iter = myUnsyncedLastUpdated.entrySet().iterator(); iter.hasNext(); ) { for (Iterator<Map.Entry<Long, Date>> iter = myUnsyncedLastUpdated.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<Long, Date> next = iter.next(); Map.Entry<Long, Date> next = iter.next();
flushLastUpdated(next.getKey(), next.getValue()); flushLastUpdated(next.getKey(), next.getValue());
@ -60,5 +73,15 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
protected abstract void flushLastUpdated(Long theSearchId, Date theLastUpdated); protected abstract void flushLastUpdated(Long theSearchId, Date theLastUpdated);
public static class SubmitJob implements Job {
@Autowired
private ISearchCacheSvc myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.flushLastUpdated();
}
}
} }

View File

@ -136,7 +136,7 @@ public class DatabaseSearchCacheSvcImpl extends BaseSearchCacheSvcImpl {
@Override @Override
public Collection<Search> findCandidatesForReuse(String theResourceType, String theQueryString, int theQueryStringHash, Date theCreatedAfter) { public Collection<Search> findCandidatesForReuse(String theResourceType, String theQueryString, int theQueryStringHash, Date theCreatedAfter) {
int hashCode = theQueryString.hashCode(); int hashCode = theQueryString.hashCode();
return mySearchDao.find(theResourceType, hashCode, theCreatedAfter); return mySearchDao.findWithCutoffOrExpiry(theResourceType, hashCode, theCreatedAfter);
} }
@ -166,7 +166,7 @@ public class DatabaseSearchCacheSvcImpl extends BaseSearchCacheSvcImpl {
TransactionTemplate tt = new TransactionTemplate(myTxManager); TransactionTemplate tt = new TransactionTemplate(myTxManager);
final Slice<Long> toDelete = tt.execute(theStatus -> final Slice<Long> toDelete = tt.execute(theStatus ->
mySearchDao.findWhereLastReturnedBefore(cutoff, PageRequest.of(0, 2000)) mySearchDao.findWhereLastReturnedBefore(cutoff, new Date(), PageRequest.of(0, 2000))
); );
for (final Long nextSearchToDelete : toDelete) { for (final Long nextSearchToDelete : toDelete) {
ourLog.debug("Deleting search with PID {}", nextSearchToDelete); ourLog.debug("Deleting search with PID {}", nextSearchToDelete);

View File

@ -36,11 +36,6 @@ public interface IResourceReindexingSvc {
*/ */
Long 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 * @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. * already proceeding. Otherwise, returns the number of resources affected.

View File

@ -33,6 +33,8 @@ import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
@ -44,12 +46,13 @@ import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.search.util.impl.Executors; import org.hibernate.search.util.impl.Executors;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.InstantType;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
@ -101,6 +104,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
private EntityManager myEntityManager; private EntityManager myEntityManager;
@Autowired @Autowired
private ISearchParamRegistry mySearchParamRegistry; private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private ISchedulerService mySchedulerService;
@VisibleForTesting @VisibleForTesting
void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) { void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) {
@ -182,11 +187,12 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
return job.getId(); return job.getId();
} }
@Override @PostConstruct
@Transactional(Transactional.TxType.NEVER) public void registerScheduledJob() {
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
public void scheduleReindexingPass() { jobDetail.setId(ResourceReindexingSvcImpl.class.getName());
runReindexingPass(); jobDetail.setJobClass(ResourceReindexingSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(10 * DateUtils.MILLIS_PER_SECOND, true, jobDetail);
} }
@Override @Override
@ -223,6 +229,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
@Override @Override
public void cancelAndPurgeAllJobs() { public void cancelAndPurgeAllJobs() {
ourLog.info("Cancelling and purging all resource reindexing jobs"); ourLog.info("Cancelling and purging all resource reindexing jobs");
myIndexingLock.lock();
try {
myTxTemplate.execute(t -> { myTxTemplate.execute(t -> {
myReindexJobDao.markAllOfTypeAsDeleted(); myReindexJobDao.markAllOfTypeAsDeleted();
return null; return null;
@ -232,6 +240,9 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
initExecutor(); initExecutor();
expungeJobsMarkedAsDeleted(); expungeJobsMarkedAsDeleted();
} finally {
myIndexingLock.unlock();
}
} }
private int runReindexJobs() { private int runReindexJobs() {
@ -277,7 +288,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
} }
@VisibleForTesting @VisibleForTesting
public void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) { void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry; mySearchParamRegistry = theSearchParamRegistry;
} }
@ -306,7 +317,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME; Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME;
Date high = theJob.getThresholdHigh(); Date high = theJob.getThresholdHigh();
// SqlQuery for resources within threshold // Query for resources within threshold
StopWatch pageSw = new StopWatch(); StopWatch pageSw = new StopWatch();
Slice<Long> range = myTxTemplate.execute(t -> { Slice<Long> range = myTxTemplate.execute(t -> {
PageRequest page = PageRequest.of(0, PASS_SIZE); PageRequest page = PageRequest.of(0, PASS_SIZE);
@ -529,4 +540,14 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
return myUpdated; return myUpdated;
} }
} }
public static class SubmitJob implements Job {
@Autowired
private IResourceReindexingSvc myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.runReindexingPass();
}
}
} }

View File

@ -26,22 +26,29 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.sched.FireAtIntervalJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.ArrayList; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component @Component
public class CacheWarmingSvcImpl implements ICacheWarmingSvc { public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
public static final long SCHEDULED_JOB_INTERVAL = 10 * DateUtils.MILLIS_PER_SECOND;
private static final Logger ourLog = LoggerFactory.getLogger(CacheWarmingSvcImpl.class);
@Autowired @Autowired
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
private Map<WarmCacheEntry, Long> myCacheEntryToNextRefresh = new LinkedHashMap<>(); private Map<WarmCacheEntry, Long> myCacheEntryToNextRefresh = new LinkedHashMap<>();
@ -51,10 +58,12 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private DaoRegistry myDaoRegistry; private DaoRegistry myDaoRegistry;
@Autowired @Autowired
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired
private ISchedulerService mySchedulerService;
@Override @Override
@Scheduled(fixedDelay = 1000)
public synchronized void performWarmingPass() { public synchronized void performWarmingPass() {
ourLog.trace("Starting cache warming pass for {} tasks", myCacheEntryToNextRefresh.size());
for (WarmCacheEntry nextCacheEntry : new ArrayList<>(myCacheEntryToNextRefresh.keySet())) { for (WarmCacheEntry nextCacheEntry : new ArrayList<>(myCacheEntryToNextRefresh.keySet())) {
@ -74,6 +83,14 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
} }
@PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(CacheWarmingSvcImpl.class.getName());
jobDetail.setJobClass(CacheWarmingSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(SCHEDULED_JOB_INTERVAL, true, jobDetail);
}
private void refreshNow(WarmCacheEntry theCacheEntry) { private void refreshNow(WarmCacheEntry theCacheEntry) {
String nextUrl = theCacheEntry.getUrl(); String nextUrl = theCacheEntry.getUrl();
@ -98,7 +115,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
initCacheMap(); initCacheMap();
} }
public synchronized void initCacheMap() { public synchronized Set<WarmCacheEntry> initCacheMap() {
myCacheEntryToNextRefresh.clear(); myCacheEntryToNextRefresh.clear();
List<WarmCacheEntry> warmCacheEntries = myDaoConfig.getWarmCacheEntries(); List<WarmCacheEntry> warmCacheEntries = myDaoConfig.getWarmCacheEntries();
@ -111,5 +128,23 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
myCacheEntryToNextRefresh.put(next, 0L); myCacheEntryToNextRefresh.put(next, 0L);
} }
return Collections.unmodifiableSet(myCacheEntryToNextRefresh.keySet());
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class SubmitJob extends FireAtIntervalJob {
@Autowired
private ICacheWarmingSvc myTarget;
public SubmitJob() {
super(SCHEDULED_JOB_INTERVAL);
}
@Override
protected void doExecute(JobExecutionContext theContext) {
myTarget.performWarmingPass();
}
} }
} }

View File

@ -20,9 +20,6 @@ package ca.uhn.fhir.jpa.search.warm;
* #L% * #L%
*/ */
import org.springframework.scheduling.annotation.Scheduled;
public interface ICacheWarmingSvc { public interface ICacheWarmingSvc {
@Scheduled(fixedDelay = 1000)
void performWarmingPass(); void performWarmingPass();
} }

View File

@ -30,4 +30,6 @@ import java.util.List;
public interface ISubscriptionTriggeringSvc { public interface ISubscriptionTriggeringSvc {
IBaseParameters triggerSubscription(List<UriParam> theResourceIds, List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId); IBaseParameters triggerSubscription(List<UriParam> theResourceIds, List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId);
void runDeliveryPass();
} }

View File

@ -25,6 +25,9 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.sched.FireAtIntervalJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@ -53,10 +56,12 @@ import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -73,10 +78,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service @Service
public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc { public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc {
public static final long SCHEDULE_DELAY = DateUtils.MILLIS_PER_SECOND;
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class); private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class);
private static final int DEFAULT_MAX_SUBMIT = 10000; private static final int DEFAULT_MAX_SUBMIT = 10000;
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
@Autowired @Autowired
private FhirContext myFhirContext; private FhirContext myFhirContext;
@Autowired @Autowired
@ -89,10 +94,10 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired @Autowired
private IResourceModifiedConsumer myResourceModifiedConsumer; private IResourceModifiedConsumer myResourceModifiedConsumer;
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT; private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
private ExecutorService myExecutorService; private ExecutorService myExecutorService;
@Autowired
private ISchedulerService mySchedulerService;
@Override @Override
public IBaseParameters triggerSubscription(List<UriParam> theResourceIds, List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId) { public IBaseParameters triggerSubscription(List<UriParam> theResourceIds, List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId) {
@ -143,8 +148,8 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
// Submit job for processing // Submit job for processing
synchronized (myActiveJobs) { synchronized (myActiveJobs) {
myActiveJobs.add(jobDetails); myActiveJobs.add(jobDetails);
ourLog.info("Subscription triggering requested for {} resource and {} search - Gave job ID: {} and have {} jobs", resourceIds.size(), searchUrls.size(), jobDetails.getJobId(), myActiveJobs.size());
} }
ourLog.info("Subscription triggering requested for {} resource and {} search - Gave job ID: {}", resourceIds.size(), searchUrls.size(), jobDetails.getJobId());
// Create a parameters response // Create a parameters response
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext); IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
@ -154,10 +159,19 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
return retVal; return retVal;
} }
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_SECOND) @PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(SubscriptionTriggeringSvcImpl.class.getName());
jobDetail.setJobClass(SubscriptionTriggeringSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(SCHEDULE_DELAY, false, jobDetail);
}
@Override
public void runDeliveryPass() { public void runDeliveryPass() {
synchronized (myActiveJobs) { synchronized (myActiveJobs) {
if (myActiveJobs.isEmpty()) { if (myActiveJobs.isEmpty()) {
return; return;
} }
@ -375,6 +389,22 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
} }
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class SubmitJob extends FireAtIntervalJob {
@Autowired
private ISubscriptionTriggeringSvc myTarget;
public SubmitJob() {
super(SCHEDULE_DELAY);
}
@Override
protected void doExecute(JobExecutionContext theContext) {
myTarget.runDeliveryPass();
}
}
private static class SubscriptionTriggeringJobDetails { private static class SubscriptionTriggeringJobDetails {
private String myJobId; private String myJobId;

View File

@ -28,6 +28,9 @@ import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -61,6 +64,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome; import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -162,6 +167,8 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
private PlatformTransactionManager myTxManager; private PlatformTransactionManager myTxManager;
@Autowired @Autowired
private ITermValueSetConceptViewDao myTermValueSetConceptViewDao; private ITermValueSetConceptViewDao myTermValueSetConceptViewDao;
@Autowired
private ISchedulerService mySchedulerService;
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) { private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) {
String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri(); String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
@ -1529,7 +1536,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
} }
} }
@Scheduled(fixedRate = 5000)
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public synchronized void saveDeferred() { public synchronized void saveDeferred() {
@ -1616,6 +1622,24 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
myTxTemplate = new TransactionTemplate(myTransactionManager); myTxTemplate = new TransactionTemplate(myTransactionManager);
} }
@PostConstruct
public void registerScheduledJob() {
// Register scheduled job to save deferred concepts
// In the future it would be great to make this a cluster-aware task somehow
ScheduledJobDefinition jobDefinition = new ScheduledJobDefinition();
jobDefinition.setId(BaseHapiTerminologySvcImpl.class.getName() + "_saveDeferred");
jobDefinition.setJobClass(SaveDeferredJob.class);
mySchedulerService.scheduleFixedDelay(5000, false, jobDefinition);
// Register scheduled job to save deferred concepts
// In the future it would be great to make this a cluster-aware task somehow
ScheduledJobDefinition vsJobDefinition = new ScheduledJobDefinition();
vsJobDefinition.setId(BaseHapiTerminologySvcImpl.class.getName() + "_preExpandValueSets");
vsJobDefinition.setJobClass(PreExpandValueSetsJob.class);
mySchedulerService.scheduleFixedDelay(10 * DateUtils.MILLIS_PER_MINUTE, true, vsJobDefinition);
}
@Override @Override
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public void storeNewCodeSystemVersion(Long theCodeSystemResourcePid, String theSystemUri, String theSystemName, String theSystemVersionId, TermCodeSystemVersion theCodeSystemVersion) { public void storeNewCodeSystemVersion(Long theCodeSystemResourcePid, String theSystemUri, String theSystemName, String theSystemVersionId, TermCodeSystemVersion theCodeSystemVersion) {
@ -1696,7 +1720,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
ourLog.info("Saving {} concepts...", totalCodeCount); ourLog.info("Saving {} concepts...", totalCodeCount);
IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<TermConcept, Object>(); IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<>();
for (TermConcept next : theCodeSystemVersion.getConcepts()) { for (TermConcept next : theCodeSystemVersion.getConcepts()) {
persistChildren(next, codeSystemVersion, conceptsStack2, totalCodeCount); persistChildren(next, codeSystemVersion, conceptsStack2, totalCodeCount);
} }
@ -1939,7 +1963,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
ourLog.info("Done storing TermConceptMap[{}]", termConceptMap.getId()); ourLog.info("Done storing TermConceptMap[{}]", termConceptMap.getId());
} }
@Scheduled(fixedDelay = 600000) // 10 minutes.
@Override @Override
public synchronized void preExpandDeferredValueSetsToTerminologyTables() { public synchronized void preExpandDeferredValueSetsToTerminologyTables() {
if (isNotSafeToPreExpandValueSets()) { if (isNotSafeToPreExpandValueSets()) {
@ -2497,6 +2520,28 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
return new VersionIndependentConcept(system, code); return new VersionIndependentConcept(system, code);
} }
public static class SaveDeferredJob implements Job {
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Override
public void execute(JobExecutionContext theContext) {
myTerminologySvc.saveDeferred();
}
}
public static class PreExpandValueSetsJob implements Job {
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Override
public void execute(JobExecutionContext theContext) {
myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables();
}
}
/** /**
* This method is present only for unit tests, do not call from client code * This method is present only for unit tests, do not call from client code
*/ */

View File

@ -0,0 +1,75 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* Smile CDR - CDR
* %%
* Copyright (C) 2016 - 2018 Simpatico Intelligent Systems Inc
* %%
* All rights reserved.
* #L%
*/
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
public class JsonUtil {
private static final ObjectMapper ourMapperPrettyPrint;
private static final ObjectMapper ourMapperNonPrettyPrint;
static {
ourMapperPrettyPrint = new ObjectMapper();
ourMapperPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL);
ourMapperPrettyPrint.enable(SerializationFeature.INDENT_OUTPUT);
ourMapperNonPrettyPrint = new ObjectMapper();
ourMapperNonPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL);
ourMapperNonPrettyPrint.disable(SerializationFeature.INDENT_OUTPUT);
}
/**
* Parse JSON
*/
public static <T> T deserialize(@Nonnull String theInput, @Nonnull Class<T> theType) throws IOException {
return ourMapperPrettyPrint.readerFor(theType).readValue(theInput);
}
/**
* Encode JSON
*/
public static String serialize(@Nonnull Object theInput) throws IOException {
return serialize(theInput, true);
}
/**
* Encode JSON
*/
public static String serialize(@Nonnull Object theInput, boolean thePrettyPrint) throws IOException {
StringWriter sw = new StringWriter();
if (thePrettyPrint) {
ourMapperPrettyPrint.writeValue(sw, theInput);
} else {
ourMapperNonPrettyPrint.writeValue(sw, theInput);
}
return sw.toString();
}
/**
* Encode JSON
*/
public static void serialize(@Nonnull Object theInput, @Nonnull Writer theWriter) throws IOException {
// Note: We append a string here rather than just having ourMapper write directly
// to the Writer because ourMapper seems to close the writer for some stupid
// reason.. There's probably a way of preventing that bit I'm not sure what that
// is and it's not a big deal here.
theWriter.append(serialize(theInput));
}
}

View File

@ -20,14 +20,106 @@ package ca.uhn.fhir.jpa.util;
* #L% * #L%
*/ */
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.time.DateUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
public class ResourceCountCache {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceCountCache.class);
private static Long ourNowForUnitTest;
private final Callable<Map<String, Long>> myFetcher;
private volatile long myCacheMillis;
private AtomicReference<Map<String, Long>> myCapabilityStatement = new AtomicReference<>();
private long myLastFetched;
@Autowired
private ISchedulerService mySchedulerService;
public class ResourceCountCache extends SingleItemLoadingCache<Map<String, Long>> {
/** /**
* Constructor * Constructor
*/ */
public ResourceCountCache(Callable<Map<String, Long>> theFetcher) { public ResourceCountCache(Callable<Map<String, Long>> theFetcher) {
super(theFetcher); myFetcher = theFetcher;
} }
public synchronized void clear() {
ourLog.info("Clearing cache");
myCapabilityStatement.set(null);
myLastFetched = 0;
}
public synchronized Map<String, Long> get() {
return myCapabilityStatement.get();
}
private Map<String, Long> refresh() {
Map<String, Long> retVal;
try {
retVal = myFetcher.call();
} catch (Exception e) {
throw new InternalErrorException(e);
}
myCapabilityStatement.set(retVal);
myLastFetched = now();
return retVal;
}
public void setCacheMillis(long theCacheMillis) {
myCacheMillis = theCacheMillis;
}
public void update() {
if (myCacheMillis > 0) {
long now = now();
long expiry = now - myCacheMillis;
if (myLastFetched < expiry) {
refresh();
}
}
}
@PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(ResourceCountCache.class.getName());
jobDetail.setJobClass(ResourceCountCache.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(10 * DateUtils.MILLIS_PER_MINUTE, false, jobDetail);
}
public static class SubmitJob implements Job {
@Autowired
private ResourceCountCache myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.update();
}
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
} }

View File

@ -1,101 +0,0 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
/**
* This is a simple cache for CapabilityStatement resources to
* be returned as server metadata.
*/
public class SingleItemLoadingCache<T> {
private static final Logger ourLog = LoggerFactory.getLogger(SingleItemLoadingCache.class);
private static Long ourNowForUnitTest;
private final Callable<T> myFetcher;
private volatile long myCacheMillis;
private AtomicReference<T> myCapabilityStatement = new AtomicReference<>();
private long myLastFetched;
/**
* Constructor
*/
public SingleItemLoadingCache(Callable<T> theFetcher) {
myFetcher = theFetcher;
}
public synchronized void clear() {
ourLog.info("Clearing cache");
myCapabilityStatement.set(null);
myLastFetched = 0;
}
public synchronized T get() {
return myCapabilityStatement.get();
}
private T refresh() {
T retVal;
try {
retVal = myFetcher.call();
} catch (Exception e) {
throw new InternalErrorException(e);
}
myCapabilityStatement.set(retVal);
myLastFetched = now();
return retVal;
}
public void setCacheMillis(long theCacheMillis) {
myCacheMillis = theCacheMillis;
}
@Scheduled(fixedDelay = 60000)
public void update() {
if (myCacheMillis > 0) {
long now = now();
long expiry = now - myCacheMillis;
if (myLastFetched < expiry) {
refresh();
}
}
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
}

View File

@ -259,7 +259,7 @@ public class TestUtil {
} }
} }
public static void sleepAtLeast(int theMillis) { public static void sleepAtLeast(long theMillis) {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
while (System.currentTimeMillis() <= start + theMillis) { while (System.currentTimeMillis() <= start + theMillis) {
try { try {

View File

@ -0,0 +1,242 @@
package ca.uhn.fhir.jpa.bulk;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.util.JsonUtil;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.apache.ResourceEntity;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.JettyUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class BulkDataExportProviderTest {
private static final String A_JOB_ID = "0000000-AAAAAA";
private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportProviderTest.class);
private Server myServer;
private FhirContext myCtx = FhirContext.forR4();
private int myPort;
@Mock
private IBulkDataExportSvc myBulkDataExportSvc;
private CloseableHttpClient myClient;
@Captor
private ArgumentCaptor<String> myOutputFormatCaptor;
@Captor
private ArgumentCaptor<Set<String>> myResourceTypesCaptor;
@Captor
private ArgumentCaptor<Date> mySinceCaptor;
@Captor
private ArgumentCaptor<Set<String>> myFiltersCaptor;
@After
public void after() throws Exception {
JettyUtil.closeServer(myServer);
myClient.close();
}
@Before
public void start() throws Exception {
myServer = new Server(0);
BulkDataExportProvider provider = new BulkDataExportProvider();
provider.setBulkDataExportSvcForUnitTests(myBulkDataExportSvc);
provider.setFhirContextForUnitTest(myCtx);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(myCtx);
servlet.registerProvider(provider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
myServer.setHandler(proxyHandler);
JettyUtil.startServer(myServer);
myPort = JettyUtil.getPortForStartedServer(myServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
myClient = builder.build();
}
@Test
public void testSuccessfulInitiateBulkRequest() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID);
when(myBulkDataExportSvc.submitJob(any(), any(), any(), any())).thenReturn(jobInfo);
InstantType now = InstantType.now();
Parameters input = new Parameters();
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType("Patient, Practitioner"));
input.addParameter(JpaConstants.PARAM_EXPORT_SINCE, now);
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE_FILTER, new StringType("Patient?identifier=foo"));
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
try (CloseableHttpResponse response = myClient.execute(post)) {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
verify(myBulkDataExportSvc, times(1)).submitJob(myOutputFormatCaptor.capture(), myResourceTypesCaptor.capture(), mySinceCaptor.capture(), myFiltersCaptor.capture());
assertEquals(Constants.CT_FHIR_NDJSON, myOutputFormatCaptor.getValue());
assertThat(myResourceTypesCaptor.getValue(), containsInAnyOrder("Patient", "Practitioner"));
assertThat(mySinceCaptor.getValue(), notNullValue());
assertThat(myFiltersCaptor.getValue(), containsInAnyOrder("Patient?identifier=foo"));
}
@Test
public void testPollForStatus_BUILDING() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID)
.setStatus(BulkJobStatusEnum.BUILDING)
.setStatusTime(InstantType.now().getValue());
when(myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
try (CloseableHttpResponse response = myClient.execute(get)) {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("120", response.getFirstHeader(Constants.HEADER_RETRY_AFTER).getValue());
assertThat(response.getFirstHeader(Constants.HEADER_X_PROGRESS).getValue(), containsString("Build in progress - Status set to BUILDING at 20"));
}
}
@Test
public void testPollForStatus_ERROR() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID)
.setStatus(BulkJobStatusEnum.ERROR)
.setStatusTime(InstantType.now().getValue())
.setStatusMessage("Some Error Message");
when(myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
try (CloseableHttpResponse response = myClient.execute(get)) {
ourLog.info("Response: {}", response.toString());
assertEquals(500, response.getStatusLine().getStatusCode());
assertEquals("Server Error", response.getStatusLine().getReasonPhrase());
String responseContent = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response content: {}", responseContent);
assertThat(responseContent, containsString("\"diagnostics\": \"Some Error Message\""));
}
}
@Test
public void testPollForStatus_COMPLETED() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID)
.setStatus(BulkJobStatusEnum.COMPLETE)
.setStatusTime(InstantType.now().getValue());
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/111"));
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/222"));
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/333"));
when(myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
try (CloseableHttpResponse response = myClient.execute(get)) {
ourLog.info("Response: {}", response.toString());
assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals("OK", response.getStatusLine().getReasonPhrase());
assertEquals(Constants.CT_JSON, response.getEntity().getContentType().getValue());
String responseContent = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response content: {}", responseContent);
BulkExportResponseJson responseJson = JsonUtil.deserialize(responseContent, BulkExportResponseJson.class);
assertEquals(3, responseJson.getOutput().size());
assertEquals("Patient", responseJson.getOutput().get(0).getType());
assertEquals("http://localhost:" + myPort + "/Binary/111", responseJson.getOutput().get(0).getUrl());
assertEquals("Patient", responseJson.getOutput().get(1).getType());
assertEquals("http://localhost:" + myPort + "/Binary/222", responseJson.getOutput().get(1).getUrl());
assertEquals("Patient", responseJson.getOutput().get(2).getType());
assertEquals("http://localhost:" + myPort + "/Binary/333", responseJson.getOutput().get(2).getUrl());
}
}
@Test
public void testPollForStatus_Gone() throws IOException {
when(myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(eq(A_JOB_ID))).thenThrow(new ResourceNotFoundException("Unknown job: AAA"));
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
try (CloseableHttpResponse response = myClient.execute(get)) {
ourLog.info("Response: {}", response.toString());
String responseContent = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response content: {}", responseContent);
assertEquals(404, response.getStatusLine().getStatusCode());
assertEquals(Constants.CT_FHIR_JSON_NEW, response.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertThat(responseContent, containsString("\"diagnostics\":\"Unknown job: AAA\""));
}
}
}

View File

@ -0,0 +1,254 @@
package ca.uhn.fhir.jpa.bulk;
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao;
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao;
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity;
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.test.utilities.UnregisterScheduledProcessor;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import java.util.Date;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;
@TestPropertySource(properties = {
UnregisterScheduledProcessor.SCHEDULING_DISABLED_EQUALS_TRUE
})
public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportSvcImplR4Test.class);
@Autowired
private IBulkExportJobDao myBulkExportJobDao;
@Autowired
private IBulkExportCollectionDao myBulkExportCollectionDao;
@Autowired
private IBulkExportCollectionFileDao myBulkExportCollectionFileDao;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Test
public void testPurgeExpiredJobs() {
// Create an expired job
runInTransaction(() -> {
Binary b = new Binary();
b.setContent(new byte[]{0, 1, 2, 3});
String binaryId = myBinaryDao.create(b).getId().toUnqualifiedVersionless().getValue();
BulkExportJobEntity job = new BulkExportJobEntity();
job.setStatus(BulkJobStatusEnum.COMPLETE);
job.setExpiry(DateUtils.addHours(new Date(), -1));
job.setJobId(UUID.randomUUID().toString());
job.setRequest("$export");
myBulkExportJobDao.save(job);
BulkExportCollectionEntity collection = new BulkExportCollectionEntity();
job.getCollections().add(collection);
collection.setResourceType("Patient");
collection.setJob(job);
myBulkExportCollectionDao.save(collection);
BulkExportCollectionFileEntity file = new BulkExportCollectionFileEntity();
collection.getFiles().add(file);
file.setCollection(collection);
file.setResource(binaryId);
myBulkExportCollectionFileDao.save(file);
});
// Check that things were created
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myBulkExportJobDao.count());
assertEquals(1, myBulkExportCollectionDao.count());
assertEquals(1, myBulkExportCollectionFileDao.count());
});
// Run a purge pass
myBulkDataExportSvc.purgeExpiredFiles();
// Check that things were deleted
runInTransaction(() -> {
assertEquals(0, myResourceTableDao.count());
assertEquals(0, myBulkExportJobDao.count());
assertEquals(0, myBulkExportCollectionDao.count());
assertEquals(0, myBulkExportCollectionFileDao.count());
});
}
@Test
public void testCreateBulkLoad_InvalidOutputFormat() {
try {
myBulkDataExportSvc.submitJob(Constants.CT_FHIR_JSON_NEW, Sets.newHashSet("Patient", "Observation"), null, null);
fail();
} catch (InvalidRequestException e) {
assertEquals("Invalid output format: application/fhir+json", e.getMessage());
}
}
@Test
public void testCreateBulkLoad_NoResourceTypes() {
try {
myBulkDataExportSvc.submitJob(Constants.CT_FHIR_NDJSON, Sets.newHashSet(), null, null);
fail();
} catch (InvalidRequestException e) {
assertEquals("No resource types specified", e.getMessage());
}
}
@Test
public void testCreateBulkLoad_InvalidResourceTypes() {
try {
myBulkDataExportSvc.submitJob(Constants.CT_FHIR_NDJSON, Sets.newHashSet("Patient", "FOO"), null, null);
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown or unsupported resource type: FOO", e.getMessage());
}
}
@Test
public void testCreateBulkLoad() {
// Create some resources to load
createResources();
// Create a bulk job
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(null, Sets.newHashSet("Patient", "Observation"), null, null);
assertNotNull(jobDetails.getJobId());
// Check the status
IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(jobDetails.getJobId());
assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus());
assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Patient", status.getRequest());
// Run a scheduled pass to build the export
myBulkDataExportSvc.buildExportFiles();
// Fetch the job again
status = myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(jobDetails.getJobId());
assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus());
assertEquals(2, status.getFiles().size());
// Iterate over the files
for (IBulkDataExportSvc.FileEntry next : status.getFiles()) {
Binary nextBinary = myBinaryDao.read(next.getResourceId());
assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType());
String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8);
ourLog.info("Next contents for type {}:\n{}", next.getResourceType(), nextContents);
if ("Patient".equals(next.getResourceType())) {
assertThat(nextContents, containsString("\"value\":\"PAT0\"}]}\n"));
assertEquals(10, nextContents.split("\n").length);
} else if ("Observation".equals(next.getResourceType())) {
assertThat(nextContents, containsString("\"subject\":{\"reference\":\"Patient/PAT0\"}}\n"));
assertEquals(10, nextContents.split("\n").length);
} else {
fail(next.getResourceType());
}
}
}
@Test
public void testSubmitReusesExisting() {
// Submit
IBulkDataExportSvc.JobInfo jobDetails1 = myBulkDataExportSvc.submitJob(null, Sets.newHashSet("Patient", "Observation"), null, null);
assertNotNull(jobDetails1.getJobId());
// Submit again
IBulkDataExportSvc.JobInfo jobDetails2 = myBulkDataExportSvc.submitJob(null, Sets.newHashSet("Patient", "Observation"), null, null);
assertNotNull(jobDetails2.getJobId());
assertEquals(jobDetails1.getJobId(), jobDetails2.getJobId());
}
@Test
public void testCreateBulkLoad_WithSince() throws InterruptedException {
// Create some resources to load
createResources();
sleepUntilTimeChanges();
InstantType cutoff = InstantType.now();
sleepUntilTimeChanges();
for (int i = 10; i < 12; i++) {
Patient patient = new Patient();
patient.setId("PAT" + i);
patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i);
myPatientDao.update(patient).getId().toUnqualifiedVersionless();
}
// Create a bulk job
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(null, Sets.newHashSet("Patient", "Observation"), cutoff.getValue(), null);
assertNotNull(jobDetails.getJobId());
// Check the status
IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(jobDetails.getJobId());
assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus());
assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Patient&_since=" + cutoff.setTimeZoneZulu(true).getValueAsString(), status.getRequest());
// Run a scheduled pass to build the export
myBulkDataExportSvc.buildExportFiles();
// Fetch the job again
status = myBulkDataExportSvc.getJobStatusOrThrowResourceNotFound(jobDetails.getJobId());
assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus());
assertEquals(1, status.getFiles().size());
// Iterate over the files
for (IBulkDataExportSvc.FileEntry next : status.getFiles()) {
Binary nextBinary = myBinaryDao.read(next.getResourceId());
assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType());
String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8);
ourLog.info("Next contents for type {}:\n{}", next.getResourceType(), nextContents);
if ("Patient".equals(next.getResourceType())) {
assertThat(nextContents, containsString("\"id\":\"PAT10\""));
assertThat(nextContents, containsString("\"id\":\"PAT11\""));
assertEquals(2, nextContents.split("\n").length);
} else {
fail(next.getResourceType());
}
}
}
private void createResources() {
for (int i = 0; i < 10; i++) {
Patient patient = new Patient();
patient.setId("PAT" + i);
patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i);
IIdType patId = myPatientDao.update(patient).getId().toUnqualifiedVersionless();
Observation obs = new Observation();
obs.setId("OBS" + i);
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.getSubject().setReference(patId.getValue());
myObservationDao.update(obs);
}
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
@ -389,9 +390,10 @@ public abstract class BaseJpaTest {
return IOUtils.toString(bundleRes, Constants.CHARSET_UTF8); return IOUtils.toString(bundleRes, Constants.CHARSET_UTF8);
} }
protected static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry) { protected static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry, IBulkDataExportSvc theBulkDataExportSvc) {
theSearchCoordinatorSvc.cancelAllActiveSearches(); theSearchCoordinatorSvc.cancelAllActiveSearches();
theResourceReindexingSvc.cancelAndPurgeAllJobs(); theResourceReindexingSvc.cancelAndPurgeAllJobs();
theBulkDataExportSvc.cancelAndPurgeAllJobs();
boolean expungeEnabled = theDaoConfig.isExpungeEnabled(); boolean expungeEnabled = theDaoConfig.isExpungeEnabled();
theDaoConfig.setExpungeEnabled(true); theDaoConfig.setExpungeEnabled(true);

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.dao.dstu2; package ca.uhn.fhir.jpa.dao.dstu2;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestDstu2Config; import ca.uhn.fhir.jpa.config.TestDstu2Config;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao;
@ -185,6 +186,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
protected IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt> myValueSetDao; protected IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt> myValueSetDao;
@Autowired @Autowired
protected SubscriptionLoader mySubscriptionLoader; protected SubscriptionLoader mySubscriptionLoader;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Before @Before
public void beforeFlushFT() { public void beforeFlushFT() {
@ -202,7 +205,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Before @Before
@Transactional() @Transactional()
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.dao.dstu3; package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestDstu3Config; import ca.uhn.fhir.jpa.config.TestDstu3Config;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.data.*;
@ -256,6 +257,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao; protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao;
@Autowired @Autowired
private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3; private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@After() @After()
public void afterCleanupDao() { public void afterCleanupDao() {
@ -302,7 +305,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Before @Before
@Transactional() @Transactional()
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.jpa.dao.dstu3; package ca.uhn.fhir.jpa.dao.dstu3;
import static org.junit.Assert.assertEquals; import ca.uhn.fhir.jpa.dao.DaoConfig;
import static org.junit.Assert.fail; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.model.Organization; import org.hl7.fhir.dstu3.model.Organization;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.Reference;
@ -11,18 +12,11 @@ import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.jpa.dao.DaoConfig; import static org.junit.Assert.assertEquals;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import static org.junit.Assert.fail;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.TestUtil;
public class FhirResourceDaoDstu3ReferentialIntegrityTest extends BaseJpaDstu3Test { public class FhirResourceDaoDstu3ReferentialIntegrityTest extends BaseJpaDstu3Test {
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After @After
public void afterResetConfig() { public void afterResetConfig() {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(new DaoConfig().isEnforceReferentialIntegrityOnWrite()); myDaoConfig.setEnforceReferentialIntegrityOnWrite(new DaoConfig().isEnforceReferentialIntegrityOnWrite());
@ -95,5 +89,10 @@ public class FhirResourceDaoDstu3ReferentialIntegrityTest extends BaseJpaDstu3Te
} }
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
} }

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.dao.dstu3; package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestDstu3WithoutLuceneConfig; import ca.uhn.fhir.jpa.config.TestDstu3WithoutLuceneConfig;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
@ -149,10 +150,12 @@ public class FhirResourceDaoDstu3SearchWithLuceneDisabledTest extends BaseJpaTes
private IValidationSupport myValidationSupport; private IValidationSupport myValidationSupport;
@Autowired @Autowired
private IResourceReindexingSvc myResourceReindexingSvc; private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Before @Before
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -73,7 +73,7 @@ public class PartitionRunnerTest {
myLatch.setExpectedCount(1); myLatch.setExpectedCount(1);
myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer); myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer);
PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected()); PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected());
assertEquals(EXPUNGE_THREADNAME_1, partitionCall.threadName); assertEquals("main", partitionCall.threadName);
assertEquals(1, partitionCall.size); assertEquals(1, partitionCall.size);
} }
@ -86,7 +86,7 @@ public class PartitionRunnerTest {
myLatch.setExpectedCount(1); myLatch.setExpectedCount(1);
myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer); myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer);
PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected()); PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected());
assertEquals(EXPUNGE_THREADNAME_1, partitionCall.threadName); assertEquals("main", partitionCall.threadName);
assertEquals(2, partitionCall.size); assertEquals(2, partitionCall.size);
} }

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.data.*;
@ -315,6 +316,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
private List<Object> mySystemInterceptors; private List<Object> mySystemInterceptors;
@Autowired @Autowired
private DaoRegistry myDaoRegistry; private DaoRegistry myDaoRegistry;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@After() @After()
public void afterCleanupDao() { public void afterCleanupDao() {
@ -376,7 +379,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Before @Before
@Transactional() @Transactional()
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -13,7 +13,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.ListUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
@ -32,7 +31,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -54,9 +52,9 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
private InterceptorService myInterceptorService; private InterceptorService myInterceptorService;
private List<String> myObservationIdsOddOnly; private List<String> myObservationIdsOddOnly;
private List<String> myObservationIdsEvenOnly; private List<String> myObservationIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyBackwards; private List<String> myObservationIdsWithVersions;
private List<String> myObservationIdsBackwards;
private List<String> myPatientIdsEvenOnly; private List<String> myPatientIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyWithVersions;
@After @After
public void after() { public void after() {
@ -313,9 +311,9 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
* Note: Each observation in the observation list will appear twice in the actual * Note: Each observation in the observation list will appear twice in the actual
* returned results because we create it then update it in create50Observations() * returned results because we create it then update it in create50Observations()
*/ */
assertEquals(sort(myObservationIdsEvenOnlyBackwards.subList(0, 3), myObservationIdsEvenOnlyBackwards.subList(0, 3)), sort(returnedIdValues));
assertEquals(1, hitCount.get()); assertEquals(1, hitCount.get());
assertEquals(sort(myObservationIdsBackwards.subList(0, 5), myObservationIdsBackwards.subList(0, 5)), sort(interceptedResourceIds)); assertEquals(myObservationIdsWithVersions.subList(90, myObservationIdsWithVersions.size()), sort(interceptedResourceIds));
assertEquals(myObservationIdsEvenOnlyWithVersions.subList(44, 50), sort(returnedIdValues));
} }
@ -349,6 +347,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
private void create50Observations() { private void create50Observations() {
myPatientIds = new ArrayList<>(); myPatientIds = new ArrayList<>();
myObservationIds = new ArrayList<>(); myObservationIds = new ArrayList<>();
myObservationIdsWithVersions = new ArrayList<>();
Patient p = new Patient(); Patient p = new Patient();
p.setActive(true); p.setActive(true);
@ -370,6 +369,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
obs1.addIdentifier().setSystem("urn:system").setValue("I" + leftPad("" + i, 5, '0')); obs1.addIdentifier().setSystem("urn:system").setValue("I" + leftPad("" + i, 5, '0'));
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless(); IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
myObservationIds.add(obs1id.toUnqualifiedVersionless().getValue()); myObservationIds.add(obs1id.toUnqualifiedVersionless().getValue());
myObservationIdsWithVersions.add(obs1id.toUnqualifiedVersionless().getValue());
obs1.setId(obs1id); obs1.setId(obs1id);
if (obs1id.getIdPartAsLong() % 2 == 0) { if (obs1id.getIdPartAsLong() % 2 == 0) {
@ -378,6 +378,8 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
obs1.getSubject().setReference(oddPid); obs1.getSubject().setReference(oddPid);
} }
myObservationDao.update(obs1); myObservationDao.update(obs1);
myObservationIdsWithVersions.add(obs1id.toUnqualifiedVersionless().getValue());
} }
myPatientIdsEvenOnly = myPatientIdsEvenOnly =
@ -391,10 +393,13 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
.stream() .stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0) .filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.collect(Collectors.toList()); .collect(Collectors.toList());
myObservationIdsEvenOnlyWithVersions =
myObservationIdsWithVersions
.stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.collect(Collectors.toList());
myObservationIdsOddOnly = ListUtils.removeAll(myObservationIds, myObservationIdsEvenOnly); myObservationIdsOddOnly = ListUtils.removeAll(myObservationIds, myObservationIdsEvenOnly);
myObservationIdsBackwards = Lists.reverse(myObservationIds);
myObservationIdsEvenOnlyBackwards = Lists.reverse(myObservationIdsEvenOnly);
} }
static class PreAccessInterceptorCounting implements IAnonymousInterceptor { static class PreAccessInterceptorCounting implements IAnonymousInterceptor {

View File

@ -78,7 +78,7 @@ public class FhirResourceDaoR4CacheWarmingTest extends BaseJpaR4Test {
.setUrl("Patient?name=smith") .setUrl("Patient?name=smith")
); );
CacheWarmingSvcImpl cacheWarmingSvc = (CacheWarmingSvcImpl) myCacheWarmingSvc; CacheWarmingSvcImpl cacheWarmingSvc = (CacheWarmingSvcImpl) myCacheWarmingSvc;
cacheWarmingSvc.initCacheMap(); ourLog.info("Have {} tasks", cacheWarmingSvc.initCacheMap().size());
Patient p1 = new Patient(); Patient p1 = new Patient();
p1.setId("p1"); p1.setId("p1");

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestR4WithoutLuceneConfig; import ca.uhn.fhir.jpa.config.TestR4WithoutLuceneConfig;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -37,7 +38,6 @@ import java.util.List;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4WithoutLuceneConfig.class}) @ContextConfiguration(classes = {TestR4WithoutLuceneConfig.class})
@ -110,11 +110,13 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest {
private IFhirSystemDao<Bundle, Meta> mySystemDao; private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired @Autowired
private IResourceReindexingSvc myResourceReindexingSvc; private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Before @Before
@Transactional() @Transactional()
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -696,7 +696,8 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
myResourceReindexingSvc.markAllResourcesForReindexing("Observation"); myResourceReindexingSvc.markAllResourcesForReindexing("Observation");
assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); assertEquals(1, myResourceReindexingSvc.forceReindexingPass());
assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); myResourceReindexingSvc.forceReindexingPass();
myResourceReindexingSvc.forceReindexingPass();
assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); assertEquals(0, myResourceReindexingSvc.forceReindexingPass());
uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); uniques = myResourceIndexedCompositeStringUniqueDao.findAll();

View File

@ -48,6 +48,11 @@ public class SearchParamExtractorR4Test {
return getActiveSearchParams(theResourceName).get(theParamName); return getActiveSearchParams(theResourceName).get(theParamName);
} }
@Override
public void refreshCacheIfNecessary() {
// nothing
}
@Override @Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() { public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestR5Config; import ca.uhn.fhir.jpa.config.TestR5Config;
import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.data.*;
@ -315,6 +316,8 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
private List<Object> mySystemInterceptors; private List<Object> mySystemInterceptors;
@Autowired @Autowired
private DaoRegistry myDaoRegistry; private DaoRegistry myDaoRegistry;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@After() @After()
public void afterCleanupDao() { public void afterCleanupDao() {
@ -376,7 +379,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
@Before @Before
@Transactional() @Transactional()
public void beforePurgeDatabase() { public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }
@Before @Before

View File

@ -4,7 +4,7 @@ import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
@ -349,7 +349,7 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
Patient patient = new Patient(); Patient patient = new Patient();
patient.setActive(true); patient.setActive(true);
IIdType id = ourClient.create().resource(patient).prefer(PreferReturnEnum.REPRESENTATION).execute().getId().toUnqualifiedVersionless(); IIdType id = ourClient.create().resource(patient).prefer(PreferHeader.PreferReturnEnum.REPRESENTATION).execute().getId().toUnqualifiedVersionless();
DelegatingConsentService consentService = new DelegatingConsentService(); DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL); myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);

View File

@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Practitioner;
@ -10,16 +9,18 @@ import org.junit.AfterClass;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@SuppressWarnings("Duplicates") @SuppressWarnings("Duplicates")
@ContextConfiguration(classes = {ResourceProviderOnlySomeResourcesProvidedR4Test.OnlySomeResourcesProvidedCtxConfig.class}) @ContextConfiguration(classes = {ResourceProviderOnlySomeResourcesProvidedR4Test.OnlySomeResourcesProvidedCtxConfig.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class ResourceProviderOnlySomeResourcesProvidedR4Test extends BaseResourceProviderR4Test { public class ResourceProviderOnlySomeResourcesProvidedR4Test extends BaseResourceProviderR4Test {
@Test @Test
@ -62,6 +63,13 @@ public class ResourceProviderOnlySomeResourcesProvidedR4Test extends BaseResourc
} }
} }
@PreDestroy
public void stop() {
myDaoRegistry.setSupportedResourceTypes();
}
} }
@AfterClass @AfterClass

View File

@ -31,6 +31,7 @@ import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*; import org.apache.http.client.methods.*;
@ -491,13 +492,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
private void checkParamMissing(String paramName) throws IOException { private void checkParamMissing(String paramName) throws IOException {
HttpGet get = new HttpGet(ourServerBase + "/Observation?" + paramName + ":missing=false"); HttpGet get = new HttpGet(ourServerBase + "/Observation?" + paramName + ":missing=false");
CloseableHttpResponse resp = ourHttpClient.execute(get); CloseableHttpResponse resp = ourHttpClient.execute(get);
IOUtils.closeQuietly(resp.getEntity().getContent()); resp.getEntity().getContent().close();
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
} }
private ArrayList<IBaseResource> genResourcesOfType(Bundle theRes, Class<? extends IBaseResource> theClass) { private ArrayList<IBaseResource> genResourcesOfType(Bundle theRes, Class<? extends IBaseResource> theClass) {
ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>(); ArrayList<IBaseResource> retVal = new ArrayList<>();
for (BundleEntryComponent next : theRes.getEntry()) { for (BundleEntryComponent next : theRes.getEntry()) {
if (next.getResource() != null) { if (next.getResource() != null) {
if (theClass.isAssignableFrom(next.getResource().getClass())) { if (theClass.isAssignableFrom(next.getResource().getClass())) {
@ -531,14 +532,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info("About to perform search for: {}", theUri); ourLog.info("About to perform search for: {}", theUri);
CloseableHttpResponse response = ourHttpClient.execute(get); try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp); Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp);
ids = toUnqualifiedIdValues(bundle); ids = toUnqualifiedIdValues(bundle);
} finally {
IOUtils.closeQuietly(response);
} }
return ids; return ids;
} }
@ -547,14 +545,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
List<String> ids; List<String> ids;
HttpGet get = new HttpGet(uri); HttpGet get = new HttpGet(uri);
CloseableHttpResponse response = ourHttpClient.execute(get); try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp); Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp);
ids = toUnqualifiedVersionlessIdValues(bundle); ids = toUnqualifiedVersionlessIdValues(bundle);
} finally {
IOUtils.closeQuietly(response);
} }
return ids; return ids;
} }
@ -589,7 +584,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String resBody = IOUtils.toString(ResourceProviderR4Test.class.getResource("/r4/document-father.json"), StandardCharsets.UTF_8); String resBody = IOUtils.toString(ResourceProviderR4Test.class.getResource("/r4/document-father.json"), StandardCharsets.UTF_8);
resBody = resBody.replace("\"type\": \"document\"", "\"type\": \"transaction\""); resBody = resBody.replace("\"type\": \"document\"", "\"type\": \"transaction\"");
try { try {
client.create().resource(resBody).execute().getId(); client.create().resource(resBody).execute();
fail(); fail();
} catch (UnprocessableEntityException e) { } catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction")); assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction"));
@ -688,7 +683,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.create() .create()
.resource(p) .resource(p)
.conditionalByUrl("Patient?identifier=foo|bar") .conditionalByUrl("Patient?identifier=foo|bar")
.prefer(PreferReturnEnum.REPRESENTATION) .prefer(PreferHeader.PreferReturnEnum.REPRESENTATION)
.execute(); .execute();
assertEquals(id.getIdPart(), outcome.getId().getIdPart()); assertEquals(id.getIdPart(), outcome.getId().getIdPart());
@ -721,7 +716,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
assertEquals(resource.withVersion("2").getValue(), resp.getFirstHeader("Content-Location").getValue()); assertEquals(resource.withVersion("2").getValue(), resp.getFirstHeader("Content-Location").getValue());
} finally { } finally {
IOUtils.closeQuietly(resp); resp.close();
} }
fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute(); fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute();
@ -737,7 +732,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
assertEquals(resource.withVersion("3").getValue(), resp.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()); assertEquals(resource.withVersion("3").getValue(), resp.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
} finally { } finally {
IOUtils.closeQuietly(resp); resp.close();
} }
fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute(); fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute();
@ -754,7 +749,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try { try {
assertEquals(400, resp.getStatusLine().getStatusCode()); assertEquals(400, resp.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(resp); resp.close();
} }
fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute(); fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute();
@ -769,7 +764,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testCreateBundle() throws IOException { public void testCreateBundle() throws IOException {
String input = IOUtils.toString(getClass().getResourceAsStream("/bryn-bundle.json"), StandardCharsets.UTF_8); String input = IOUtils.toString(getClass().getResourceAsStream("/bryn-bundle.json"), StandardCharsets.UTF_8);
Validate.notNull(input); Validate.notNull(input);
ourClient.create().resource(input).execute().getResource(); ourClient.create().resource(input).execute();
} }
@Test @Test
@ -1659,7 +1654,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
b = parser.parseResource(Bundle.class, new InputStreamReader(ResourceProviderR4Test.class.getResourceAsStream("/r4/bug147-bundle.json"))); b = parser.parseResource(Bundle.class, new InputStreamReader(ResourceProviderR4Test.class.getResourceAsStream("/r4/bug147-bundle.json")));
Bundle resp = ourClient.transaction().withBundle(b).execute(); Bundle resp = ourClient.transaction().withBundle(b).execute();
List<IdType> ids = new ArrayList<IdType>(); List<IdType> ids = new ArrayList<>();
for (BundleEntryComponent next : resp.getEntry()) { for (BundleEntryComponent next : resp.getEntry()) {
IdType toAdd = new IdType(next.getResponse().getLocation()).toUnqualifiedVersionless(); IdType toAdd = new IdType(next.getResponse().getLocation()).toUnqualifiedVersionless();
ids.add(toAdd); ids.add(toAdd);
@ -1673,7 +1668,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute();
b = (Bundle) output.getParameter().get(0).getResource(); b = (Bundle) output.getParameter().get(0).getResource();
ids = new ArrayList<IdType>(); ids = new ArrayList<>();
boolean dupes = false; boolean dupes = false;
for (BundleEntryComponent next : b.getEntry()) { for (BundleEntryComponent next : b.getEntry()) {
IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless(); IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless();
@ -1694,7 +1689,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Parameters output = ourClient.operation().onInstance(patientId).named("everything").withParameters(input).execute(); Parameters output = ourClient.operation().onInstance(patientId).named("everything").withParameters(input).execute();
b = (Bundle) output.getParameter().get(0).getResource(); b = (Bundle) output.getParameter().get(0).getResource();
ids = new ArrayList<IdType>(); ids = new ArrayList<>();
boolean dupes = false; boolean dupes = false;
for (BundleEntryComponent next : b.getEntry()) { for (BundleEntryComponent next : b.getEntry()) {
IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless(); IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless();
@ -1760,7 +1755,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute();
b = (Bundle) output.getParameter().get(0).getResource(); b = (Bundle) output.getParameter().get(0).getResource();
List<IdType> ids = new ArrayList<IdType>(); List<IdType> ids = new ArrayList<>();
for (BundleEntryComponent next : b.getEntry()) { for (BundleEntryComponent next : b.getEntry()) {
IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless(); IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless();
ids.add(toAdd); ids.add(toAdd);
@ -1892,7 +1887,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try { try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
ourLog.info(output); ourLog.info(output);
List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
ourLog.info(ids.toString()); ourLog.info(ids.toString());
@ -1907,7 +1902,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try { try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
ourLog.info(output); ourLog.info(output);
List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
ourLog.info(ids.toString()); ourLog.info(ids.toString());
@ -1926,7 +1921,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// try { // try {
// assertEquals(200, response.getStatusLine().getStatusCode()); // assertEquals(200, response.getStatusLine().getStatusCode());
// String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); // String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
// IOUtils.closeQuietly(response.getEntity().getContent()); // response.getEntity().getContent().close();
// ourLog.info(output); // ourLog.info(output);
// List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); // List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
// ourLog.info(ids.toString()); // ourLog.info(ids.toString());
@ -1940,7 +1935,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// try { // try {
// assertEquals(200, response.getStatusLine().getStatusCode()); // assertEquals(200, response.getStatusLine().getStatusCode());
// String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); // String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
// IOUtils.closeQuietly(response.getEntity().getContent()); // response.getEntity().getContent().close();
// ourLog.info(output); // ourLog.info(output);
// List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); // List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
// ourLog.info(ids.toString()); // ourLog.info(ids.toString());
@ -1964,7 +1959,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(53, inputBundle.getEntry().size()); assertEquals(53, inputBundle.getEntry().size());
Set<String> allIds = new TreeSet<String>(); Set<String> allIds = new TreeSet<>();
for (BundleEntryComponent nextEntry : inputBundle.getEntry()) { for (BundleEntryComponent nextEntry : inputBundle.getEntry()) {
nextEntry.getRequest().setMethod(HTTPVerb.PUT); nextEntry.getRequest().setMethod(HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getId()); nextEntry.getRequest().setUrl(nextEntry.getResource().getId());
@ -1986,7 +1981,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(responseBundle)); ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(responseBundle));
List<String> ids = new ArrayList<String>(); List<String> ids = new ArrayList<>();
for (BundleEntryComponent nextEntry : responseBundle.getEntry()) { for (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
ids.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); ids.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
} }
@ -1995,7 +1990,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(responseBundle.getEntry().size(), lessThanOrEqualTo(25)); assertThat(responseBundle.getEntry().size(), lessThanOrEqualTo(25));
TreeSet<String> idsSet = new TreeSet<String>(); TreeSet<String> idsSet = new TreeSet<>();
for (int i = 0; i < responseBundle.getEntry().size(); i++) { for (int i = 0; i < responseBundle.getEntry().size(); i++) {
for (BundleEntryComponent nextEntry : responseBundle.getEntry()) { for (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
idsSet.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); idsSet.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
@ -2118,13 +2113,10 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
HttpPost post = new HttpPost(ourServerBase + "/Patient/" + JpaConstants.OPERATION_VALIDATE); HttpPost post = new HttpPost(ourServerBase + "/Patient/" + JpaConstants.OPERATION_VALIDATE);
post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON)); post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON));
CloseableHttpResponse resp = ourHttpClient.execute(post); try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
try {
String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8); String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(respString); ourLog.info(respString);
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(resp);
} }
} }
@ -2144,14 +2136,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
HttpPost post = new HttpPost(ourServerBase + "/Patient/$validate"); HttpPost post = new HttpPost(ourServerBase + "/Patient/$validate");
post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON)); post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON));
CloseableHttpResponse resp = ourHttpClient.execute(post); try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
try {
String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8); String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(respString); ourLog.info(respString);
assertThat(respString, containsString("Unknown extension http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode")); assertThat(respString, containsString("Unknown extension http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode"));
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(resp);
} }
} finally { } finally {
ourRestServer.unregisterInterceptor(interceptor); ourRestServer.unregisterInterceptor(interceptor);
@ -2247,15 +2236,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myResourceCountsCache.update(); myResourceCountsCache.update();
HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts"); HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts");
CloseableHttpResponse response = ourHttpClient.execute(get); try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
ourLog.info(output); ourLog.info(output);
assertThat(output, containsString("<parameter><name value=\"Patient\"/><valueInteger value=\"")); assertThat(output, containsString("<parameter><name value=\"Patient\"/><valueInteger value=\""));
} finally {
response.close();
} }
} }
@ -2300,13 +2286,10 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testHasParameterNoResults() throws Exception { public void testHasParameterNoResults() throws Exception {
HttpGet get = new HttpGet(ourServerBase + "/AllergyIntolerance?_has=Provenance:target:userID=12345"); HttpGet get = new HttpGet(ourServerBase + "/AllergyIntolerance?_has=Provenance:target:userID=12345");
CloseableHttpResponse response = ourHttpClient.execute(get); try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
assertThat(resp, containsString("Invalid _has parameter syntax: _has")); assertThat(resp, containsString("Invalid _has parameter syntax: _has"));
} finally {
IOUtils.closeQuietly(response);
} }
} }
@ -2644,7 +2627,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, stringContainsInOrder("THIS IS THE DESC")); assertThat(resp, stringContainsInOrder("THIS IS THE DESC"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -2798,7 +2781,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("Underweight")); assertThat(resp, containsString("Underweight"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
@ -2820,14 +2803,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME); patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX))); patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch); try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome")); assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION")); assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
} }
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute(); Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
@ -2851,14 +2831,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX))); patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
patch.addHeader("If-Match", "W/\"9\""); patch.addHeader("If-Match", "W/\"9\"");
CloseableHttpResponse response = ourHttpClient.execute(patch); try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
try {
assertEquals(409, response.getStatusLine().getStatusCode()); assertEquals(409, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome")); assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("<diagnostics value=\"Version 9 is not the most recent version of this resource, unable to apply patch\"/>")); assertThat(responseString, containsString("<diagnostics value=\"Version 9 is not the most recent version of this resource, unable to apply patch\"/>"));
} finally {
response.close();
} }
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute(); Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
@ -2883,14 +2860,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patch.addHeader("If-Match", "W/\"1\""); patch.addHeader("If-Match", "W/\"1\"");
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX))); patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch); try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome")); assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION")); assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
} }
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute(); Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
@ -2915,14 +2889,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME); patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX))); patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch); try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome")); assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION")); assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
} }
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute(); Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
@ -2987,11 +2958,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
pat = new Patient(); pat = new Patient();
pat.addIdentifier().setSystem("urn:system").setValue("testReadAllInstancesOfType_01"); pat.addIdentifier().setSystem("urn:system").setValue("testReadAllInstancesOfType_01");
ourClient.create().resource(pat).prettyPrint().encodedXml().execute().getId(); ourClient.create().resource(pat).prettyPrint().encodedXml().execute();
pat = new Patient(); pat = new Patient();
pat.addIdentifier().setSystem("urn:system").setValue("testReadAllInstancesOfType_02"); pat.addIdentifier().setSystem("urn:system").setValue("testReadAllInstancesOfType_02");
ourClient.create().resource(pat).prettyPrint().encodedXml().execute().getId(); ourClient.create().resource(pat).prettyPrint().encodedXml().execute();
{ {
Bundle returned = ourClient.search().forResource(Patient.class).encodedXml().returnBundle(Bundle.class).execute(); Bundle returned = ourClient.search().forResource(Patient.class).encodedXml().returnBundle(Bundle.class).execute();
@ -3115,7 +3086,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient actual = ourClient.read(Patient.class, new UriDt(newId.getValue())); Patient actual = ourClient.read(Patient.class, new UriDt(newId.getValue()));
assertEquals(1, actual.getContained().size()); assertEquals(1, actual.getContained().size());
//@formatter:off
Bundle b = ourClient Bundle b = ourClient
.search() .search()
.forResource("Patient") .forResource("Patient")
@ -3123,7 +3093,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.prettyPrint() .prettyPrint()
.returnBundle(Bundle.class) .returnBundle(Bundle.class)
.execute(); .execute();
//@formatter:on
assertEquals(1, b.getEntry().size()); assertEquals(1, b.getEntry().size());
} }
@ -3873,6 +3842,70 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(uuid1, uuid2); assertEquals(uuid1, uuid2);
} }
@Test
public void testSearchReusesBeforeExpiry() {
List<IBaseResource> resources = new ArrayList<IBaseResource>();
for (int i = 0; i < 50; i++) {
Organization org = new Organization();
org.setName("HELLO");
resources.add(org);
}
ourClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
/*
* First, make sure that we don't reuse a search if
* it's not marked with an expiry
*/
{
myDaoConfig.setReuseCachedSearchResultsForMillis(10L);
Bundle result1 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
final String uuid1 = toSearchUuidFromLinkNext(result1);
sleepOneClick();
Bundle result2 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
final String uuid2 = toSearchUuidFromLinkNext(result2);
assertNotEquals(uuid1, uuid2);
}
/*
* Now try one but mark it with an expiry time
* in the future
*/
{
myDaoConfig.setReuseCachedSearchResultsForMillis(1000L);
Bundle result1 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
final String uuid1 = toSearchUuidFromLinkNext(result1);
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid1).orElseThrow(()->new IllegalStateException());
search.setExpiryOrNull(DateUtils.addSeconds(new Date(), -2));
mySearchEntityDao.save(search);
});
sleepOneClick();
Bundle result2 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
// Expiry doesn't affect reusablility
final String uuid2 = toSearchUuidFromLinkNext(result2);
assertEquals(uuid1, uuid2);
}
}
@Test @Test
public void testSearchReusesResultsDisabled() { public void testSearchReusesResultsDisabled() {
List<IBaseResource> resources = new ArrayList<>(); List<IBaseResource> resources = new ArrayList<>();
@ -4995,13 +5028,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String encoded = myFhirCtx.newJsonParser().encodeResourceToString(p); String encoded = myFhirCtx.newJsonParser().encodeResourceToString(p);
HttpPut put = new HttpPut(ourServerBase + "/Patient/A"); HttpPut put = new HttpPut(ourServerBase + "/Patient/A");
put.setEntity(new StringEntity(encoded, "application/fhir+json", "UTF-8")); put.setEntity(new StringEntity(encoded, ContentType.create("application/fhir+json", "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(put); CloseableHttpResponse response = ourHttpClient.execute(put);
try { try {
assertEquals(201, response.getStatusLine().getStatusCode()); assertEquals(201, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response); response.close();
} }
p = new Patient(); p = new Patient();
@ -5010,13 +5043,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
encoded = myFhirCtx.newJsonParser().encodeResourceToString(p); encoded = myFhirCtx.newJsonParser().encodeResourceToString(p);
put = new HttpPut(ourServerBase + "/Patient/A"); put = new HttpPut(ourServerBase + "/Patient/A");
put.setEntity(new StringEntity(encoded, "application/fhir+json", "UTF-8")); put.setEntity(new StringEntity(encoded, ContentType.create("application/fhir+json", "UTF-8")));
response = ourHttpClient.execute(put); response = ourHttpClient.execute(put);
try { try {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response); response.close();
} }
} }
@ -5100,8 +5133,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
HttpPut post = new HttpPut(ourServerBase + "/Patient/A2"); HttpPut post = new HttpPut(ourServerBase + "/Patient/A2");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post); try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
try {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString); ourLog.info(responseString);
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(400, response.getStatusLine().getStatusCode());
@ -5109,8 +5141,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals( assertEquals(
"Can not update resource, resource body must contain an ID element which matches the request URL for update (PUT) operation - Resource body ID of \"333\" does not match URL ID of \"A2\"", "Can not update resource, resource body must contain an ID element which matches the request URL for update (PUT) operation - Resource body ID of \"333\" does not match URL ID of \"A2\"",
oo.getIssue().get(0).getDiagnostics()); oo.getIssue().get(0).getDiagnostics());
} finally {
response.close();
} }
} }
@ -5119,7 +5149,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String input = IOUtils.toString(getClass().getResourceAsStream("/dstu3-person.json"), StandardCharsets.UTF_8); String input = IOUtils.toString(getClass().getResourceAsStream("/dstu3-person.json"), StandardCharsets.UTF_8);
try { try {
MethodOutcome resp = ourClient.update().resource(input).withId("Patient/PERSON1").execute(); ourClient.update().resource(input).withId("Patient/PERSON1").execute();
} catch (InvalidRequestException e) { } catch (InvalidRequestException e) {
assertEquals("", e.getMessage()); assertEquals("", e.getMessage());
} }
@ -5139,7 +5169,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is &quot;delete&quot;)")); assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is &quot;delete&quot;)"));
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(400, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5163,7 +5193,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is &quot;delete&quot;)")); assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is &quot;delete&quot;)"));
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(400, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5189,7 +5219,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, assertThat(resp,
stringContainsInOrder(">ERROR<", "[Patient.contact[0]]", "<pre>SHALL at least contain a contact's details or a reference to an organization", "<issue><severity value=\"error\"/>")); stringContainsInOrder(">ERROR<", "[Patient.contact[0]]", "<pre>SHALL at least contain a contact's details or a reference to an organization", "<issue><severity value=\"error\"/>"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5217,7 +5247,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(resp); ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5241,7 +5271,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(412, response.getStatusLine().getStatusCode()); assertEquals(412, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("SHALL at least contain a contact's details or a reference to an organization")); assertThat(resp, containsString("SHALL at least contain a contact's details or a reference to an organization"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5270,7 +5300,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(resp); ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5298,7 +5328,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, not(containsString("warn"))); assertThat(resp, not(containsString("warn")));
assertThat(resp, not(containsString("error"))); assertThat(resp, not(containsString("error")));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5325,7 +5355,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>", stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
"</issue>")); "</issue>"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
} }
@ -5358,7 +5388,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, containsString("</contains>")); assertThat(resp, containsString("</contains>"));
assertThat(resp, containsString("</expansion>")); assertThat(resp, containsString("</expansion>"));
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }
@ -5378,7 +5408,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
"<display value=\"Systolic blood pressure at First encounter\"/>")); "<display value=\"Systolic blood pressure at First encounter\"/>"));
//@formatter:on //@formatter:on
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); response.getEntity().getContent().close();
response.close(); response.close();
} }

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
import ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl; import ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl;
import ca.uhn.fhir.rest.gclient.IClientExecutable; import ca.uhn.fhir.rest.gclient.IClientExecutable;
import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.IQuery;
@ -28,6 +29,7 @@ import org.springframework.test.util.AopTestUtils;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -68,13 +70,11 @@ public class StaleSearchDeletingSvcR4Test extends BaseResourceProviderR4Test {
myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
} }
//@formatter:off
IClientExecutable<IQuery<Bundle>, Bundle> search = ourClient IClientExecutable<IQuery<Bundle>, Bundle> search = ourClient
.search() .search()
.forResource(Patient.class) .forResource(Patient.class)
.where(Patient.NAME.matches().value("Everything")) .where(Patient.NAME.matches().value("Everything"))
.returnBundle(Bundle.class); .returnBundle(Bundle.class);
//@formatter:on
Bundle resp1 = search.execute(); Bundle resp1 = search.execute();
@ -172,6 +172,40 @@ public class StaleSearchDeletingSvcR4Test extends BaseResourceProviderR4Test {
} }
@Test
public void testDontDeleteSearchBeforeExpiry() {
DatabaseSearchCacheSvcImpl.setMaximumResultsToDeleteForUnitTest(10);
runInTransaction(() -> {
Search search = new Search();
// Expires in one second, so it should not be deleted right away,
// but it should be deleted if we try again after one second...
search.setExpiryOrNull(DateUtils.addMilliseconds(new Date(), 1000));
search.setStatus(SearchStatusEnum.FINISHED);
search.setUuid(UUID.randomUUID().toString());
search.setCreated(DateUtils.addDays(new Date(), -10000));
search.setSearchType(SearchTypeEnum.SEARCH);
search.setResourceType("Patient");
search.setSearchLastReturned(DateUtils.addDays(new Date(), -10000));
search = mySearchEntityDao.save(search);
});
// Should not delete right now
assertEquals(1, mySearchEntityDao.count());
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
assertEquals(1, mySearchEntityDao.count());
sleepAtLeast(1100);
// Now it's time to delete
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
assertEquals(0, mySearchEntityDao.count());
}
@AfterClass @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -0,0 +1,207 @@
package ca.uhn.fhir.jpa.sched;
import ca.uhn.fhir.jpa.model.sched.FireAtIntervalJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.util.ProxyUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.AopTestUtils;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@ContextConfiguration(classes = SchedulerServiceImplTest.TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SchedulerServiceImplTest {
private static final Logger ourLog = LoggerFactory.getLogger(SchedulerServiceImplTest.class);
@Autowired
private ISchedulerService mySvc;
private static long ourTaskDelay;
@Before
public void before() {
ourTaskDelay = 0;
}
@Test
public void testScheduleTask() {
ScheduledJobDefinition def = new ScheduledJobDefinition()
.setId(CountingJob.class.getName())
.setJobClass(CountingJob.class);
mySvc.scheduleFixedDelay(100, false, def);
sleepAtLeast(1000);
ourLog.info("Fired {} times", CountingJob.ourCount);
assertThat(CountingJob.ourCount, greaterThan(3));
assertThat(CountingJob.ourCount, lessThan(20));
}
@Test
public void testStopAndStartService() throws SchedulerException {
ScheduledJobDefinition def = new ScheduledJobDefinition()
.setId(CountingJob.class.getName())
.setJobClass(CountingJob.class);
SchedulerServiceImpl svc = AopTestUtils.getTargetObject(mySvc);
svc.stop();
svc.start();
svc.contextStarted(null);
mySvc.scheduleFixedDelay(100, false, def);
sleepAtLeast(1000);
ourLog.info("Fired {} times", CountingJob.ourCount);
assertThat(CountingJob.ourCount, greaterThan(3));
assertThat(CountingJob.ourCount, lessThan(20));
}
@Test
public void testScheduleTaskLongRunningDoesntRunConcurrently() {
ScheduledJobDefinition def = new ScheduledJobDefinition()
.setId(CountingJob.class.getName())
.setJobClass(CountingJob.class);
ourTaskDelay = 500;
mySvc.scheduleFixedDelay(100, false, def);
sleepAtLeast(1000);
ourLog.info("Fired {} times", CountingJob.ourCount);
assertThat(CountingJob.ourCount, greaterThanOrEqualTo(1));
assertThat(CountingJob.ourCount, lessThan(5));
}
@Test
public void testIntervalJob() {
ScheduledJobDefinition def = new ScheduledJobDefinition()
.setId(CountingIntervalJob.class.getName())
.setJobClass(CountingIntervalJob.class);
ourTaskDelay = 500;
mySvc.scheduleFixedDelay(100, false, def);
sleepAtLeast(2000);
ourLog.info("Fired {} times", CountingIntervalJob.ourCount);
assertThat(CountingIntervalJob.ourCount, greaterThanOrEqualTo(2));
assertThat(CountingIntervalJob.ourCount, lessThan(6));
}
@After
public void after() throws SchedulerException {
CountingJob.ourCount = 0;
CountingIntervalJob.ourCount = 0;
mySvc.purgeAllScheduledJobsForUnitTest();
}
@DisallowConcurrentExecution
public static class CountingJob implements Job, ApplicationContextAware {
private static int ourCount;
@Autowired
@Qualifier("stringBean")
private String myStringBean;
private ApplicationContext myAppCtx;
@Override
public void execute(JobExecutionContext theContext) {
if (!"String beans are good.".equals(myStringBean)) {
fail("Did not autowire stringBean correctly, found: " + myStringBean);
}
if (myAppCtx == null) {
fail("Did not populate appctx");
}
if (ourTaskDelay > 0) {
ourLog.info("Job has fired, going to sleep for {}ms", ourTaskDelay);
sleepAtLeast(ourTaskDelay);
ourLog.info("Done sleeping");
} else {
ourLog.info("Job has fired...");
}
ourCount++;
}
@Override
public void setApplicationContext(ApplicationContext theAppCtx) throws BeansException {
myAppCtx = theAppCtx;
}
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class CountingIntervalJob extends FireAtIntervalJob {
private static int ourCount;
@Autowired
@Qualifier("stringBean")
private String myStringBean;
private ApplicationContext myAppCtx;
public CountingIntervalJob() {
super(500);
}
@Override
public void doExecute(JobExecutionContext theContext) {
ourLog.info("Job has fired, going to sleep for {}ms", ourTaskDelay);
sleepAtLeast(ourTaskDelay);
ourCount++;
}
}
@Configuration
public static class TestConfiguration {
@Bean
public ISchedulerService schedulerService() {
return new SchedulerServiceImpl();
}
@Bean
public String stringBean() {
return "String beans are good.";
}
@Bean
public AutowiringSpringBeanJobFactory springBeanJobFactory() {
return new AutowiringSpringBeanJobFactory();
}
}
}

View File

@ -80,7 +80,4 @@ public class SubscriptionTestUtil {
subscriber.setEmailSender(myEmailSender); subscriber.setEmailSender(myEmailSender);
} }
public IEmailSender getEmailSender() {
return myEmailSender;
}
} }

View File

@ -39,6 +39,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
@Autowired @Autowired
private SubscriptionTestUtil mySubscriptionTestUtil; private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
@After @After
public void after() throws Exception { public void after() throws Exception {
ourLog.info("** AFTER **"); ourLog.info("** AFTER **");

View File

@ -408,6 +408,19 @@ public class InMemorySubscriptionMatcherR4Test {
} }
} }
@Test
public void testReferenceAlias() {
Observation obs = new Observation();
obs.setId("Observation/123");
obs.getSubject().setReference("Patient/123");
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Observation.SP_PATIENT, new ReferenceParam("Patient/123"));
assertMatched(obs, params);
}
@Test @Test
public void testSearchResourceReferenceOnlyCorrectPath() { public void testSearchResourceReferenceOnlyCorrectPath() {
Organization org = new Organization(); Organization org = new Organization();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
@ -55,6 +56,10 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
@Autowired @Autowired
private SubscriptionTestUtil mySubscriptionTestUtil; private SubscriptionTestUtil mySubscriptionTestUtil;
@Autowired
private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc;
@Autowired
private ISchedulerService mySchedulerService;
@After @After
public void afterUnregisterRestHookListener() { public void afterUnregisterRestHookListener() {
@ -80,9 +85,6 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
} }
@Autowired
private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc;
@Before @Before
public void beforeRegisterRestHookListener() { public void beforeRegisterRestHookListener() {
mySubscriptionTestUtil.registerRestHookInterceptor(); mySubscriptionTestUtil.registerRestHookInterceptor();
@ -98,6 +100,8 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
ourCreatedPatients.clear(); ourCreatedPatients.clear();
ourUpdatedPatients.clear(); ourUpdatedPatients.clear();
ourContentTypes.clear(); ourContentTypes.clear();
mySchedulerService.logStatus();
} }
private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException {

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.util; package ca.uhn.fhir.jpa.util;
import org.hl7.fhir.dstu3.model.CapabilityStatement;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -8,29 +7,31 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class SingleItemLoadingCacheTest { public class ResourceCountCacheTest {
@Mock @Mock
private Callable<CapabilityStatement> myFetcher; private Callable<Map<String, Long>> myFetcher;
@After @After
public void after() { public void after() {
SingleItemLoadingCache.setNowForUnitTest(null); ResourceCountCache.setNowForUnitTest(null);
} }
@Before @Before
public void before() throws Exception { public void before() throws Exception {
AtomicInteger id = new AtomicInteger(); AtomicLong id = new AtomicLong();
when(myFetcher.call()).thenAnswer(t->{ when(myFetcher.call()).thenAnswer(t->{
CapabilityStatement retVal = new CapabilityStatement(); Map<String, Long> retVal = new HashMap<>();
retVal.setId("" + id.incrementAndGet()); retVal.put("A", id.incrementAndGet());
return retVal; return retVal;
}); });
} }
@ -38,36 +39,36 @@ public class SingleItemLoadingCacheTest {
@Test @Test
public void testCache() { public void testCache() {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
SingleItemLoadingCache.setNowForUnitTest(start); ResourceCountCache.setNowForUnitTest(start);
// Cache is initialized on startup // Cache is initialized on startup
SingleItemLoadingCache<CapabilityStatement> cache = new SingleItemLoadingCache<>(myFetcher); ResourceCountCache cache = new ResourceCountCache(myFetcher);
cache.setCacheMillis(500); cache.setCacheMillis(500);
assertEquals(null, cache.get()); assertEquals(null, cache.get());
// Not time to update yet // Not time to update yet
cache.update(); cache.update();
assertEquals("1", cache.get().getId()); assertEquals(Long.valueOf(1), cache.get().get("A"));
// Wait a bit, still not time to update // Wait a bit, still not time to update
SingleItemLoadingCache.setNowForUnitTest(start + 400); ResourceCountCache.setNowForUnitTest(start + 400);
cache.update(); cache.update();
assertEquals("1", cache.get().getId()); assertEquals(Long.valueOf(1), cache.get().get("A"));
// Wait a bit more and the cache is expired // Wait a bit more and the cache is expired
SingleItemLoadingCache.setNowForUnitTest(start + 800); ResourceCountCache.setNowForUnitTest(start + 800);
cache.update(); cache.update();
assertEquals("2", cache.get().getId()); assertEquals(Long.valueOf(2), cache.get().get("A"));
} }
@Test @Test
public void testCacheWithLoadingDisabled() { public void testCacheWithLoadingDisabled() {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
SingleItemLoadingCache.setNowForUnitTest(start); ResourceCountCache.setNowForUnitTest(start);
// Cache of 0 means "never load" // Cache of 0 means "never load"
SingleItemLoadingCache<CapabilityStatement> cache = new SingleItemLoadingCache<>(myFetcher); ResourceCountCache cache = new ResourceCountCache(myFetcher);
cache.setCacheMillis(0); cache.setCacheMillis(0);
/* /*
@ -79,11 +80,11 @@ public class SingleItemLoadingCacheTest {
cache.update(); cache.update();
assertEquals(null, cache.get()); assertEquals(null, cache.get());
SingleItemLoadingCache.setNowForUnitTest(start + 400); ResourceCountCache.setNowForUnitTest(start + 400);
cache.update(); cache.update();
assertEquals(null, cache.get()); assertEquals(null, cache.get());
SingleItemLoadingCache.setNowForUnitTest(start + 80000); ResourceCountCache.setNowForUnitTest(start + 80000);
cache.update(); cache.update();
assertEquals(null, cache.get()); assertEquals(null, cache.get());

View File

@ -109,6 +109,16 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.addIndex("IDX_VS_CONCEPT_ORDER") .addIndex("IDX_VS_CONCEPT_ORDER")
.unique(true) .unique(true)
.withColumns("VALUESET_PID", "VALUESET_ORDER"); .withColumns("VALUESET_PID", "VALUESET_ORDER");
// Account for RESTYPE_LEN column increasing from 30 to 35
version.onTable("HFJ_RESOURCE").modifyColumn("RES_TYPE").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 35);
version.onTable("HFJ_HISTORY_TAG").modifyColumn("RES_TYPE").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 35);
version.onTable("HFJ_RES_LINK").modifyColumn("SOURCE_RESOURCE_TYPE").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 35);
version.onTable("HFJ_RES_LINK").modifyColumn("TARGET_RESOURCE_TYPE").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 35);
version.onTable("HFJ_RES_TAG").modifyColumn("RES_TYPE").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 35);
} }
protected void init400() { protected void init400() {

View File

@ -112,6 +112,11 @@
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- Java --> <!-- Java -->
<dependency> <dependency>
<groupId>javax.annotation</groupId> <groupId>javax.annotation</groupId>

View File

@ -47,7 +47,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
@Index(name = "IDX_INDEXSTATUS", columnList = "SP_INDEX_STATUS") @Index(name = "IDX_INDEXSTATUS", columnList = "SP_INDEX_STATUS")
}) })
public class ResourceTable extends BaseHasResource implements Serializable { public class ResourceTable extends BaseHasResource implements Serializable {
static final int RESTYPE_LEN = 30; public static final int RESTYPE_LEN = 35;
private static final int MAX_LANGUAGE_LENGTH = 20; private static final int MAX_LANGUAGE_LENGTH = 20;
private static final int MAX_PROFILE_LENGTH = 200; private static final int MAX_PROFILE_LENGTH = 200;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -199,7 +199,7 @@ public class ResourceTable extends BaseHasResource implements Serializable {
@OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) @OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false)
@OptimisticLock(excluded = true) @OptimisticLock(excluded = true)
private Collection<ResourceLink> myResourceLinksAsTarget; private Collection<ResourceLink> myResourceLinksAsTarget;
@Column(name = "RES_TYPE", length = RESTYPE_LEN) @Column(name = "RES_TYPE", length = RESTYPE_LEN, nullable = false)
@Field @Field
@OptimisticLock(excluded = true) @OptimisticLock(excluded = true)
private String myResourceType; private String myResourceType;

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.jpa.model.sched;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public abstract class FireAtIntervalJob implements Job {
public static final String NEXT_EXECUTION_TIME = "NEXT_EXECUTION_TIME";
private static final Logger ourLog = LoggerFactory.getLogger(FireAtIntervalJob.class);
private final long myMillisBetweenExecutions;
public FireAtIntervalJob(long theMillisBetweenExecutions) {
myMillisBetweenExecutions = theMillisBetweenExecutions;
}
@Override
public final void execute(JobExecutionContext theContext) {
Long nextExecution = (Long) theContext.getJobDetail().getJobDataMap().get(NEXT_EXECUTION_TIME);
if (nextExecution != null) {
long cutoff = System.currentTimeMillis();
if (nextExecution >= cutoff) {
return;
}
}
try {
doExecute(theContext);
} catch (Throwable t) {
ourLog.error("Job threw uncaught exception", t);
} finally {
long newNextExecution = System.currentTimeMillis() + myMillisBetweenExecutions;
theContext.getJobDetail().getJobDataMap().put(NEXT_EXECUTION_TIME, newNextExecution);
}
}
protected abstract void doExecute(JobExecutionContext theContext);
}

View File

@ -0,0 +1,21 @@
package ca.uhn.fhir.jpa.model.sched;
import com.google.common.annotations.VisibleForTesting;
import org.quartz.SchedulerException;
public interface ISchedulerService {
@VisibleForTesting
void purgeAllScheduledJobsForUnitTest() throws SchedulerException;
void logStatus();
/**
* @param theIntervalMillis How many milliseconds between passes should this job run
* @param theClusteredTask If <code>true</code>, only one instance of this task will fire across the whole cluster (when running in a clustered environment). If <code>false</code>, or if not running in a clustered environment, this task will execute locally (and should execute on all nodes of the cluster)
* @param theJobDefinition The Job to fire
*/
void scheduleFixedDelay(long theIntervalMillis, boolean theClusteredTask, ScheduledJobDefinition theJobDefinition);
boolean isStopping();
}

View File

@ -0,0 +1,51 @@
package ca.uhn.fhir.jpa.model.sched;
import org.apache.commons.lang3.Validate;
import org.quartz.Job;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class ScheduledJobDefinition {
private Class<? extends Job> myJobClass;
private String myId;
private Map<String, String> myJobData;
public Map<String, String> getJobData() {
Map<String, String> retVal = myJobData;
if (retVal == null) {
retVal = Collections.emptyMap();
}
return Collections.unmodifiableMap(retVal);
}
public Class<? extends Job> getJobClass() {
return myJobClass;
}
public ScheduledJobDefinition setJobClass(Class<? extends Job> theJobClass) {
myJobClass = theJobClass;
return this;
}
public String getId() {
return myId;
}
public ScheduledJobDefinition setId(String theId) {
myId = theId;
return this;
}
public void addJobData(String thePropertyName, String thePropertyValue) {
Validate.notBlank(thePropertyName);
if (myJobData == null) {
myJobData = new HashMap<>();
}
Validate.isTrue(myJobData.containsKey(thePropertyName) == false);
myJobData.put(thePropertyName, thePropertyValue);
}
}

View File

@ -174,6 +174,16 @@ public class JpaConstants {
*/ */
public static final String OPERATION_UPLOAD_EXTERNAL_CODE_SYSTEM = "$upload-external-code-system"; public static final String OPERATION_UPLOAD_EXTERNAL_CODE_SYSTEM = "$upload-external-code-system";
/**
* Operation name for the "$export" operation
*/
public static final String OPERATION_EXPORT = "$export";
/**
* Operation name for the "$export-poll-status" operation
*/
public static final String OPERATION_EXPORT_POLL_STATUS = "$export-poll-status";
/** /**
* <p> * <p>
* This extension should be of type <code>string</code> and should be * This extension should be of type <code>string</code> and should be
@ -238,5 +248,28 @@ public class JpaConstants {
*/ */
public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source"; public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source";
/**
* Parameter for the $export operation
*/
public static final String PARAM_EXPORT_POLL_STATUS_JOB_ID = "_jobId";
/**
* Parameter for the $export operation
*/
public static final String PARAM_EXPORT_OUTPUT_FORMAT = "_outputFormat";
/**
* Parameter for the $export operation
*/
public static final String PARAM_EXPORT_TYPE = "_type";
/**
* Parameter for the $export operation
*/
public static final String PARAM_EXPORT_SINCE = "_since";
/**
* Parameter for the $export operation
*/
public static final String PARAM_EXPORT_TYPE_FILTER = "_typeFilter";
} }

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