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.util.Date;
import ca.uhn.fhir.rest.api.PreferHeader;
import org.hl7.fhir.dstu3.model.*;
import org.junit.*;
import org.mockito.ArgumentCaptor;
@ -20,7 +21,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
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.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
@ -246,7 +246,7 @@ public class GenericClientDstu3IT {
Patient pt = new 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());
assertNotNull(outcome.getResource());

View File

@ -220,17 +220,31 @@ public class Constants {
public static final String CASCADE_DELETE = "delete";
public static final int MAX_RESOURCE_NAME_LENGTH = 100;
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 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.
*/
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 {
CHARSET_UTF8 = StandardCharsets.UTF_8;
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<>();
statusNames.put(200, "OK");
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.PreferReturnEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutcome> {
@ -47,6 +47,6 @@ public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutc
*
* @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.PreferReturnEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
public interface IPatchExecutable extends IClientExecutable<IPatchExecutable, MethodOutcome>{
@ -32,6 +32,6 @@ public interface IPatchExecutable extends IClientExecutable<IPatchExecutable, Me
*
* @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.PreferReturnEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
public interface IUpdateExecutable extends IClientExecutable<IUpdateExecutable, MethodOutcome>{
@ -32,6 +32,6 @@ public interface IUpdateExecutable extends IClientExecutable<IUpdateExecutable,
*
* @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();
}
public StopWatch(long theL) {
}
private void addNewlineIfContentExists(StringBuilder theB) {
if (theB.length() > 0) {
theB.append("\n");
@ -231,7 +234,12 @@ public class StopWatch {
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() {

View File

@ -5,9 +5,9 @@
if you are using this file as a basis for your own project. -->
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<artifactId>hapi-deployable-pom</artifactId>
<version>4.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>
<artifactId>hapi-fhir-cli-jpaserver</artifactId>
@ -131,6 +131,10 @@
<artifactId>Saxon-HE</artifactId>
<groupId>net.sf.saxon</groupId>
</exclusion>
<exclusion>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-core</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
@ -183,15 +187,6 @@
</configuration>
</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 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -1,5 +1,25 @@
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.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;

View File

@ -1,5 +1,25 @@
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.jpa.dao.DaoConfig;
import org.apache.commons.cli.ParseException;

View File

@ -1,5 +1,25 @@
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.dao.DaoConfig;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2;

View File

@ -1,5 +1,25 @@
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.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;

View File

@ -1,5 +1,25 @@
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.dao.DaoConfig;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorR4;

View File

@ -1,5 +1,25 @@
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.Configuration;
import org.springframework.context.annotation.Import;

View File

@ -1,5 +1,25 @@
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.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;

View File

@ -364,11 +364,11 @@ public class GenericOkHttpClientDstu2Test {
Patient p = new Patient();
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(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(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.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(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(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 boolean myConditional;
private PreferReturnEnum myPrefer;
private PreferHeader.PreferReturnEnum myPrefer;
private IBaseResource myResource;
private String myResourceBody;
private String mySearchUrl;
@ -580,7 +580,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public ICreateTyped prefer(PreferReturnEnum theReturn) {
public ICreateTyped prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn;
return this;
}
@ -1380,13 +1380,13 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private PreferReturnEnum myPrefer;
private PreferHeader.PreferReturnEnum myPrefer;
private OutcomeResponseHandler() {
super();
}
private OutcomeResponseHandler(PreferReturnEnum thePrefer) {
private OutcomeResponseHandler(PreferHeader.PreferReturnEnum thePrefer) {
this();
myPrefer = thePrefer;
}
@ -1396,7 +1396,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
response.setCreatedUsingStatusCode(theResponseStatusCode);
if (myPrefer == PreferReturnEnum.REPRESENTATION) {
if (myPrefer == PreferHeader.PreferReturnEnum.REPRESENTATION) {
if (response.getResource() == null) {
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());
@ -1418,7 +1418,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private IIdType myId;
private String myPatchBody;
private PatchTypeEnum myPatchType;
private PreferReturnEnum myPrefer;
private PreferHeader.PreferReturnEnum myPrefer;
private String myResourceType;
private String mySearchUrl;
@ -1476,7 +1476,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IPatchExecutable prefer(PreferReturnEnum theReturn) {
public IPatchExecutable prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn;
return this;
}
@ -2048,7 +2048,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private boolean myConditional;
private IIdType myId;
private PreferReturnEnum myPrefer;
private PreferHeader.PreferReturnEnum myPrefer;
private IBaseResource myResource;
private String myResourceBody;
private String mySearchUrl;
@ -2102,7 +2102,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IUpdateExecutable prefer(PreferReturnEnum theReturn) {
public IUpdateExecutable prefer(PreferHeader.PreferReturnEnum theReturn) {
myPrefer = theReturn;
return this;
}
@ -2282,7 +2282,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
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) {
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
public PreferReturnEnum getDefaultPreferReturn() {
return PreferReturnEnum.REPRESENTATION;
public PreferHeader.PreferReturnEnum getDefaultPreferReturn() {
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.Response;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
@ -371,8 +370,8 @@ implements IRestfulServer<JaxRsRequest>, IResourceProvider {
}
@Override
public PreferReturnEnum getDefaultPreferReturn() {
return PreferReturnEnum.REPRESENTATION;
public PreferHeader.PreferReturnEnum getDefaultPreferReturn() {
return PreferHeader.PreferReturnEnum.REPRESENTATION;
}
/**

View File

@ -301,12 +301,12 @@ public class GenericJaxRsClientDstu2Test {
Patient p = new Patient();
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(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(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.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(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(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();
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(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(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.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(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(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.TestJaxRsMockPatientRestProviderDstu3;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.EncodingEnum;
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.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -138,7 +135,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
client.setEncoding(EncodingEnum.JSON);
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());
IBaseResource resource = response.getResource();
@ -161,7 +158,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome);
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();
IBaseResource resource = response.getResource();
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.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.EncodingEnum;
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.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -134,7 +131,7 @@ public class AbstractJaxRsResourceProviderTest {
client.setEncoding(EncodingEnum.JSON);
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());
IResource resource = (IResource) response.getResource();
@ -157,7 +154,7 @@ public class AbstractJaxRsResourceProviderTest {
when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome);
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();
IResource resource = (IResource) response.getResource();
compareResultId(1, resource);

View File

@ -138,7 +138,7 @@ public class JaxRsPatientProviderDstu3Test {
existing.setId((IdType) null);
existing.getName().add(new HumanName().setFamily("Created Patient 54"));
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());
final Patient patient = (Patient) results.getResource();
System.out.println(patient);
@ -154,7 +154,7 @@ public class JaxRsPatientProviderDstu3Test {
existing.setId((IdType) null);
existing.getName().add(new HumanName().setFamily("Created Patient 54"));
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());
final Patient patient = (Patient) results.getResource();
@ -187,7 +187,7 @@ public class JaxRsPatientProviderDstu3Test {
public void testDeletePatient() {
final Patient existing = new Patient();
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());
final Patient patient = (Patient) results.getResource();
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.rest.api.EncodingEnum;
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.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@ -152,7 +152,7 @@ public class JaxRsPatientProviderR4Test {
existing.setId((IdDt) null);
existing.getNameFirstRep().setFamily("Created Patient 54");
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());
final Patient patient = (Patient) results.getResource();
System.out.println(patient);
@ -167,7 +167,7 @@ public class JaxRsPatientProviderR4Test {
existing.setId((IdDt) null);
existing.getNameFirstRep().setFamily("Created Patient 54");
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());
final Patient patient = (Patient) results.getResource();
@ -199,7 +199,7 @@ public class JaxRsPatientProviderR4Test {
public void testDeletePatient() {
final Patient existing = new Patient();
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());
final Patient patient = (Patient) results.getResource();
client.delete().resource(patient).execute();

View File

@ -149,7 +149,7 @@ public class JaxRsPatientProviderTest {
existing.setId((IdDt) null);
existing.getNameFirstRep().addFamily("Created Patient 54");
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());
final Patient patient = (Patient) results.getResource();
System.out.println(patient);
@ -164,7 +164,7 @@ public class JaxRsPatientProviderTest {
existing.setId((IdDt) null);
existing.getNameFirstRep().addFamily("Created Patient 54");
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());
final Patient patient = (Patient) results.getResource();
@ -196,7 +196,7 @@ public class JaxRsPatientProviderTest {
public void testDeletePatient() {
final Patient existing = new Patient();
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());
final Patient patient = (Patient) results.getResource();
client.delete().resourceById(patient.getId()).execute();

View File

@ -309,6 +309,11 @@
<scope>provided</scope>
</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> -->
@ -700,7 +705,7 @@
<configPackageBase>ca.uhn.fhir.jpa.config</configPackageBase>
<packageBase>ca.uhn.fhir.jpa.rp.dstu2</packageBase>
<targetResourceSpringBeansFile>hapi-fhir-server-resourceproviders-dstu2.xml</targetResourceSpringBeansFile>
<baseResourceNames></baseResourceNames>
<baseResourceNames/>
<excludeResourceNames>
<!-- <excludeResourceName>OperationDefinition</excludeResourceName> <excludeResourceName>OperationOutcome</excludeResourceName> -->
</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.jpa.binstore.BinaryAccessProvider;
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.graphql.JpaStorageServices;
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.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.IStaleSearchDeletingSvc;
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 org.hibernate.jpa.HibernatePersistenceProvider;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
@ -38,15 +43,10 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import javax.annotation.Nonnull;
/*
* #%L
* HAPI FHIR JPA Server
@ -69,7 +69,6 @@ import javax.annotation.Nonnull;
@Configuration
@EnableScheduling
@EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data")
@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BaseConfig.class),
@ -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 = "ca.uhn.fhir.jpa.subscription.module.standalone.*")})
public abstract class BaseConfig implements SchedulingConfigurer {
public abstract class BaseConfig {
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider";
@ -86,18 +84,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Autowired
protected Environment myEnv;
@Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) {
theTaskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean("myDaoRegistry")
public DaoRegistry daoRegistry() {
return new DaoRegistry();
}
@Bean(autowire = Autowire.BY_TYPE)
@Bean
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
return new DatabaseBackedPagingProvider();
}
@ -226,7 +218,7 @@ public abstract class BaseConfig implements SchedulingConfigurer {
* Subclasses may override
*/
protected boolean isSupported(String theResourceType) {
return daoRegistry().getResourceDaoIfExists(theResourceType) != null;
return daoRegistry().getResourceDaoOrNull(theResourceType) != null;
}
@Bean
@ -241,6 +233,30 @@ public abstract class BaseConfig implements SchedulingConfigurer {
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) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
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)
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
protected PlatformTransactionManager myPlatformTransactionManager;
@ -551,10 +551,22 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
myEntityManager.merge(entity);
}
private void validateExpungeEnabled() {
if (!myDaoConfig.isExpungeEnabled()) {
throw new MethodNotAllowedException("$expunge is not enabled on this server");
}
}
@Override
@Transactional(propagation = Propagation.NEVER)
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);
BaseHasResource entity = txTemplate.execute(t -> readEntity(theId, theRequest));

View File

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

View File

@ -116,6 +116,8 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
ExpungeOutcome expunge(IIdType theIIdType, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
Class<T> getResourceType();
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 (outcome.getResource() != null) {
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 == PreferReturnEnum.REPRESENTATION) {
if (preferReturn == PreferHeader.PreferReturnEnum.REPRESENTATION) {
outcome.fireResourceViewCallbacks();
myVersionAdapter.setResource(newEntry, outcome.getResource());
}
@ -211,7 +211,10 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
String nextReplacementIdPart = nextReplacementId.getValueAsString();
if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) {
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")
Optional<Search> findByUuidAndFetchIncludes(@Param("uuid") String theUuid);
@Query("SELECT s.myId FROM Search s WHERE s.mySearchLastReturned < :cutoff")
Slice<Long> findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, Pageable thePage);
@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, @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")
Collection<Search> find(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff);
@Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND (s.myCreated > :cutoff) AND s.myDeleted = false")
Collection<Search> findWithCutoffOrExpiry(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff);
@Modifying
@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) {
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) {
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;
}
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());
try {
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;
@Column(name = "FAILURE_MESSAGE", length = FAILURE_MESSAGE_LENGTH, nullable = true)
private String myFailureMessage;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "EXPIRY_OR_NULL", nullable = true)
private Date myExpiryOrNull;
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCH")
@SequenceGenerator(name = "SEQ_SEARCH", sequenceName = "SEQ_SEARCH")
@ -108,7 +111,6 @@ public class Search implements ICachedSearchDetails, Serializable {
@Lob
@Column(name = "SEARCH_PARAM_MAP", nullable = true)
private byte[] mySearchParameterMap;
/**
* Constructor
*/
@ -116,6 +118,14 @@ public class Search implements ICachedSearchDetails, Serializable {
super();
}
public Date getExpiryOrNull() {
return myExpiryOrNull;
}
public void setExpiryOrNull(Date theExpiryOrNull) {
myExpiryOrNull = theExpiryOrNull;
}
public Boolean getDeleted() {
return myDeleted;
}
@ -230,11 +240,15 @@ public class Search implements ICachedSearchDetails, Serializable {
}
public void setSearchQueryString(String theSearchQueryString) {
if (theSearchQueryString != null && theSearchQueryString.length() > MAX_SEARCH_QUERY_STRING) {
mySearchQueryString = null;
if (theSearchQueryString == null || theSearchQueryString.length() > MAX_SEARCH_QUERY_STRING) {
// 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 {
mySearchQueryString = theSearchQueryString;
}
mySearchQueryStringHash = mySearchQueryString.hashCode();
}
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.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
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.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_CUTOFF_SLACK;
/**
@ -43,6 +48,8 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
private DaoConfig myDaoConfig;
@Autowired
private ISearchCacheSvc mySearchCacheSvc;
@Autowired
private ISchedulerService mySchedulerService;
@Override
@Transactional(propagation = Propagation.NEVER)
@ -50,7 +57,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
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)
@Override
public synchronized void schedulePollForStaleSearches() {
@ -58,4 +72,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
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.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
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.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
@ -36,6 +40,8 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private ISchedulerService mySchedulerService;
private ConcurrentHashMap<Long, Date> myUnsyncedLastUpdated = new ConcurrentHashMap<>();
@ -44,11 +50,18 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
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
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
public void flushLastUpdated() {
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.execute(t->{
txTemplate.execute(t -> {
for (Iterator<Map.Entry<Long, Date>> iter = myUnsyncedLastUpdated.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<Long, Date> next = iter.next();
flushLastUpdated(next.getKey(), next.getValue());
@ -60,5 +73,15 @@ public abstract class BaseSearchCacheSvcImpl implements ISearchCacheSvc {
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
public Collection<Search> findCandidatesForReuse(String theResourceType, String theQueryString, int theQueryStringHash, Date theCreatedAfter) {
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);
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) {
ourLog.debug("Deleting search with PID {}", nextSearchToDelete);

View File

@ -36,11 +36,6 @@ public interface IResourceReindexingSvc {
*/
Long markAllResourcesForReindexing(String theType);
/**
* Called automatically by the job scheduler
*/
void scheduleReindexingPass();
/**
* @return Returns null if the system did not attempt to perform a pass because one was
* already proceeding. Otherwise, returns the number of resources affected.

View File

@ -33,6 +33,8 @@ import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
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.rest.server.exceptions.InternalErrorException;
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.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.InstantType;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
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.Slice;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionCallback;
@ -101,6 +104,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
private EntityManager myEntityManager;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private ISchedulerService mySchedulerService;
@VisibleForTesting
void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) {
@ -182,11 +187,12 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
return job.getId();
}
@Override
@Transactional(Transactional.TxType.NEVER)
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
public void scheduleReindexingPass() {
runReindexingPass();
@PostConstruct
public void registerScheduledJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(ResourceReindexingSvcImpl.class.getName());
jobDetail.setJobClass(ResourceReindexingSvcImpl.SubmitJob.class);
mySchedulerService.scheduleFixedDelay(10 * DateUtils.MILLIS_PER_SECOND, true, jobDetail);
}
@Override
@ -223,6 +229,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
@Override
public void cancelAndPurgeAllJobs() {
ourLog.info("Cancelling and purging all resource reindexing jobs");
myIndexingLock.lock();
try {
myTxTemplate.execute(t -> {
myReindexJobDao.markAllOfTypeAsDeleted();
return null;
@ -232,6 +240,9 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
initExecutor();
expungeJobsMarkedAsDeleted();
} finally {
myIndexingLock.unlock();
}
}
private int runReindexJobs() {
@ -277,7 +288,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
}
@VisibleForTesting
public void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) {
void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry;
}
@ -306,7 +317,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME;
Date high = theJob.getThresholdHigh();
// SqlQuery for resources within threshold
// Query for resources within threshold
StopWatch pageSw = new StopWatch();
Slice<Long> range = myTxTemplate.execute(t -> {
PageRequest page = PageRequest.of(0, PASS_SIZE);
@ -529,4 +540,14 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
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.DaoRegistry;
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.SearchParameterMap;
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.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
@Component
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
private DaoConfig myDaoConfig;
private Map<WarmCacheEntry, Long> myCacheEntryToNextRefresh = new LinkedHashMap<>();
@ -51,10 +58,12 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private ISchedulerService mySchedulerService;
@Override
@Scheduled(fixedDelay = 1000)
public synchronized void performWarmingPass() {
ourLog.trace("Starting cache warming pass for {} tasks", myCacheEntryToNextRefresh.size());
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) {
String nextUrl = theCacheEntry.getUrl();
@ -98,7 +115,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
initCacheMap();
}
public synchronized void initCacheMap() {
public synchronized Set<WarmCacheEntry> initCacheMap() {
myCacheEntryToNextRefresh.clear();
List<WarmCacheEntry> warmCacheEntries = myDaoConfig.getWarmCacheEntries();
@ -111,5 +128,23 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
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%
*/
import org.springframework.scheduling.annotation.Scheduled;
public interface ICacheWarmingSvc {
@Scheduled(fixedDelay = 1000)
void performWarmingPass();
}

View File

@ -30,4 +30,6 @@ import java.util.List;
public interface ISubscriptionTriggeringSvc {
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.DaoRegistry;
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.search.ISearchCoordinatorSvc;
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.IIdType;
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.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@ -73,10 +78,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
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 int DEFAULT_MAX_SUBMIT = 10000;
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
@Autowired
private FhirContext myFhirContext;
@Autowired
@ -89,10 +94,10 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceModifiedConsumer myResourceModifiedConsumer;
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
private ExecutorService myExecutorService;
@Autowired
private ISchedulerService mySchedulerService;
@Override
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
synchronized (myActiveJobs) {
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
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
@ -154,10 +159,19 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
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() {
synchronized (myActiveJobs) {
if (myActiveJobs.isEmpty()) {
return;
}
@ -305,7 +319,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
return myExecutorService.submit(() -> {
for (int i = 0; ; i++) {
try {
myResourceModifiedConsumer.submitResourceModified(msg);
myResourceModifiedConsumer.submitResourceModified(msg);
break;
} catch (Exception e) {
if (i >= 3) {
@ -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 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.TermConceptParentChildLink.RelationshipTypeEnum;
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.rest.api.server.RequestDetails;
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.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
@ -162,6 +167,8 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
private PlatformTransactionManager myTxManager;
@Autowired
private ITermValueSetConceptViewDao myTermValueSetConceptViewDao;
@Autowired
private ISchedulerService mySchedulerService;
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) {
String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
@ -1529,7 +1536,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
}
}
@Scheduled(fixedRate = 5000)
@Transactional(propagation = Propagation.NEVER)
@Override
public synchronized void saveDeferred() {
@ -1616,6 +1622,24 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
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
@Transactional(propagation = Propagation.REQUIRED)
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);
IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<TermConcept, Object>();
IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<>();
for (TermConcept next : theCodeSystemVersion.getConcepts()) {
persistChildren(next, codeSystemVersion, conceptsStack2, totalCodeCount);
}
@ -1939,7 +1963,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
ourLog.info("Done storing TermConceptMap[{}]", termConceptMap.getId());
}
@Scheduled(fixedDelay = 600000) // 10 minutes.
@Override
public synchronized void preExpandDeferredValueSetsToTerminologyTables() {
if (isNotSafeToPreExpandValueSets()) {
@ -2497,6 +2520,28 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
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
*/

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%
*/
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.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
*/
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();
while (System.currentTimeMillis() <= start + theMillis) {
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.Pointcut;
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.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
@ -389,9 +390,10 @@ public abstract class BaseJpaTest {
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();
theResourceReindexingSvc.cancelAndPurgeAllJobs();
theBulkDataExportSvc.cancelAndPurgeAllJobs();
boolean expungeEnabled = theDaoConfig.isExpungeEnabled();
theDaoConfig.setExpungeEnabled(true);

View File

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

View File

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

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import ca.uhn.fhir.jpa.dao.DaoConfig;
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.Patient;
import org.hl7.fhir.dstu3.model.Reference;
@ -11,18 +12,11 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.TestUtil;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class FhirResourceDaoDstu3ReferentialIntegrityTest extends BaseJpaDstu3Test {
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After
public void afterResetConfig() {
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;
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.dao.*;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
@ -149,10 +150,12 @@ public class FhirResourceDaoDstu3SearchWithLuceneDisabledTest extends BaseJpaTes
private IValidationSupport myValidationSupport;
@Autowired
private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Before
public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry);
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
}
@Before

View File

@ -73,7 +73,7 @@ public class PartitionRunnerTest {
myLatch.setExpectedCount(1);
myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer);
PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected());
assertEquals(EXPUNGE_THREADNAME_1, partitionCall.threadName);
assertEquals("main", partitionCall.threadName);
assertEquals(1, partitionCall.size);
}
@ -86,7 +86,7 @@ public class PartitionRunnerTest {
myLatch.setExpectedCount(1);
myPartitionRunner.runInPartitionedThreads(resourceIds, partitionConsumer);
PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(myLatch.awaitExpected());
assertEquals(EXPUNGE_THREADNAME_1, partitionCall.threadName);
assertEquals("main", partitionCall.threadName);
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.jpa.binstore.BinaryAccessProvider;
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.dao.*;
import ca.uhn.fhir.jpa.dao.data.*;
@ -315,6 +316,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
private List<Object> mySystemInterceptors;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@After()
public void afterCleanupDao() {
@ -376,7 +379,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Before
@Transactional()
public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry);
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
}
@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.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.ListUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -32,7 +31,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -54,9 +52,9 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
private InterceptorService myInterceptorService;
private List<String> myObservationIdsOddOnly;
private List<String> myObservationIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyBackwards;
private List<String> myObservationIdsBackwards;
private List<String> myObservationIdsWithVersions;
private List<String> myPatientIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyWithVersions;
@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
* 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(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() {
myPatientIds = new ArrayList<>();
myObservationIds = new ArrayList<>();
myObservationIdsWithVersions = new ArrayList<>();
Patient p = new Patient();
p.setActive(true);
@ -370,6 +369,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
obs1.addIdentifier().setSystem("urn:system").setValue("I" + leftPad("" + i, 5, '0'));
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
myObservationIds.add(obs1id.toUnqualifiedVersionless().getValue());
myObservationIdsWithVersions.add(obs1id.toUnqualifiedVersionless().getValue());
obs1.setId(obs1id);
if (obs1id.getIdPartAsLong() % 2 == 0) {
@ -378,6 +378,8 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
obs1.getSubject().setReference(oddPid);
}
myObservationDao.update(obs1);
myObservationIdsWithVersions.add(obs1id.toUnqualifiedVersionless().getValue());
}
myPatientIdsEvenOnly =
@ -391,10 +393,13 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
.stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.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);
myObservationIdsBackwards = Lists.reverse(myObservationIds);
myObservationIdsEvenOnlyBackwards = Lists.reverse(myObservationIdsEvenOnly);
}
static class PreAccessInterceptorCounting implements IAnonymousInterceptor {

View File

@ -78,7 +78,7 @@ public class FhirResourceDaoR4CacheWarmingTest extends BaseJpaR4Test {
.setUrl("Patient?name=smith")
);
CacheWarmingSvcImpl cacheWarmingSvc = (CacheWarmingSvcImpl) myCacheWarmingSvc;
cacheWarmingSvc.initCacheMap();
ourLog.info("Have {} tasks", cacheWarmingSvc.initCacheMap().size());
Patient p1 = new Patient();
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.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.TestR4WithoutLuceneConfig;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -37,7 +38,6 @@ import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4WithoutLuceneConfig.class})
@ -110,11 +110,13 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest {
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired
private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Before
@Transactional()
public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry);
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
}
@Before

View File

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

View File

@ -48,6 +48,11 @@ public class SearchParamExtractorR4Test {
return getActiveSearchParams(theResourceName).get(theParamName);
}
@Override
public void refreshCacheIfNecessary() {
// nothing
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
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.jpa.binstore.BinaryAccessProvider;
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.dao.*;
import ca.uhn.fhir.jpa.dao.data.*;
@ -315,6 +316,8 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
private List<Object> mySystemInterceptors;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@After()
public void afterCleanupDao() {
@ -376,7 +379,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
@Before
@Transactional()
public void beforePurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry);
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
}
@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.dao.DaoConfig;
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.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
@ -349,7 +349,7 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
Patient patient = new Patient();
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();
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.util.TestUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
@ -10,16 +9,18 @@ import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
@SuppressWarnings("Duplicates")
@ContextConfiguration(classes = {ResourceProviderOnlySomeResourcesProvidedR4Test.OnlySomeResourcesProvidedCtxConfig.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class ResourceProviderOnlySomeResourcesProvidedR4Test extends BaseResourceProviderR4Test {
@Test
@ -62,6 +63,13 @@ public class ResourceProviderOnlySomeResourcesProvidedR4Test extends BaseResourc
}
}
@PreDestroy
public void stop() {
myDaoRegistry.setSupportedResourceTypes();
}
}
@AfterClass

View File

@ -31,6 +31,7 @@ import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
@ -491,13 +492,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
private void checkParamMissing(String paramName) throws IOException {
HttpGet get = new HttpGet(ourServerBase + "/Observation?" + paramName + ":missing=false");
CloseableHttpResponse resp = ourHttpClient.execute(get);
IOUtils.closeQuietly(resp.getEntity().getContent());
resp.getEntity().getContent().close();
assertEquals(200, resp.getStatusLine().getStatusCode());
}
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()) {
if (next.getResource() != null) {
if (theClass.isAssignableFrom(next.getResource().getClass())) {
@ -531,14 +532,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info("About to perform search for: {}", theUri);
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp);
ids = toUnqualifiedIdValues(bundle);
} finally {
IOUtils.closeQuietly(response);
}
return ids;
}
@ -547,14 +545,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
List<String> ids;
HttpGet get = new HttpGet(uri);
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp);
ids = toUnqualifiedVersionlessIdValues(bundle);
} finally {
IOUtils.closeQuietly(response);
}
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);
resBody = resBody.replace("\"type\": \"document\"", "\"type\": \"transaction\"");
try {
client.create().resource(resBody).execute().getId();
client.create().resource(resBody).execute();
fail();
} catch (UnprocessableEntityException e) {
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()
.resource(p)
.conditionalByUrl("Patient?identifier=foo|bar")
.prefer(PreferReturnEnum.REPRESENTATION)
.prefer(PreferHeader.PreferReturnEnum.REPRESENTATION)
.execute();
assertEquals(id.getIdPart(), outcome.getId().getIdPart());
@ -721,7 +716,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, resp.getStatusLine().getStatusCode());
assertEquals(resource.withVersion("2").getValue(), resp.getFirstHeader("Content-Location").getValue());
} finally {
IOUtils.closeQuietly(resp);
resp.close();
}
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(resource.withVersion("3").getValue(), resp.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
} finally {
IOUtils.closeQuietly(resp);
resp.close();
}
fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute();
@ -754,7 +749,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try {
assertEquals(400, resp.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(resp);
resp.close();
}
fromDB = ourClient.read().resource(Binary.class).withId(resource.toVersionless()).execute();
@ -769,7 +764,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testCreateBundle() throws IOException {
String input = IOUtils.toString(getClass().getResourceAsStream("/bryn-bundle.json"), StandardCharsets.UTF_8);
Validate.notNull(input);
ourClient.create().resource(input).execute().getResource();
ourClient.create().resource(input).execute();
}
@Test
@ -1659,7 +1654,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
b = parser.parseResource(Bundle.class, new InputStreamReader(ResourceProviderR4Test.class.getResourceAsStream("/r4/bug147-bundle.json")));
Bundle resp = ourClient.transaction().withBundle(b).execute();
List<IdType> ids = new ArrayList<IdType>();
List<IdType> ids = new ArrayList<>();
for (BundleEntryComponent next : resp.getEntry()) {
IdType toAdd = new IdType(next.getResponse().getLocation()).toUnqualifiedVersionless();
ids.add(toAdd);
@ -1673,7 +1668,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute();
b = (Bundle) output.getParameter().get(0).getResource();
ids = new ArrayList<IdType>();
ids = new ArrayList<>();
boolean dupes = false;
for (BundleEntryComponent next : b.getEntry()) {
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();
b = (Bundle) output.getParameter().get(0).getResource();
ids = new ArrayList<IdType>();
ids = new ArrayList<>();
boolean dupes = false;
for (BundleEntryComponent next : b.getEntry()) {
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();
b = (Bundle) output.getParameter().get(0).getResource();
List<IdType> ids = new ArrayList<IdType>();
List<IdType> ids = new ArrayList<>();
for (BundleEntryComponent next : b.getEntry()) {
IdType toAdd = next.getResource().getIdElement().toUnqualifiedVersionless();
ids.add(toAdd);
@ -1892,7 +1887,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
ourLog.info(output);
List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
ourLog.info(ids.toString());
@ -1907,7 +1902,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
ourLog.info(output);
List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
ourLog.info(ids.toString());
@ -1926,7 +1921,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// try {
// assertEquals(200, response.getStatusLine().getStatusCode());
// String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
// IOUtils.closeQuietly(response.getEntity().getContent());
// response.getEntity().getContent().close();
// ourLog.info(output);
// List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
// ourLog.info(ids.toString());
@ -1940,7 +1935,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// try {
// assertEquals(200, response.getStatusLine().getStatusCode());
// String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
// IOUtils.closeQuietly(response.getEntity().getContent());
// response.getEntity().getContent().close();
// ourLog.info(output);
// List<IIdType> ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output));
// ourLog.info(ids.toString());
@ -1964,7 +1959,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(53, inputBundle.getEntry().size());
Set<String> allIds = new TreeSet<String>();
Set<String> allIds = new TreeSet<>();
for (BundleEntryComponent nextEntry : inputBundle.getEntry()) {
nextEntry.getRequest().setMethod(HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getId());
@ -1986,7 +1981,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(responseBundle));
List<String> ids = new ArrayList<String>();
List<String> ids = new ArrayList<>();
for (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
ids.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
@ -1995,7 +1990,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
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 (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
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);
post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON));
CloseableHttpResponse resp = ourHttpClient.execute(post);
try {
try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(respString);
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");
post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON));
CloseableHttpResponse resp = ourHttpClient.execute(post);
try {
try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(respString);
assertThat(respString, containsString("Unknown extension http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode"));
assertEquals(200, resp.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(resp);
}
} finally {
ourRestServer.unregisterInterceptor(interceptor);
@ -2247,15 +2236,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myResourceCountsCache.update();
HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
ourLog.info(output);
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 {
HttpGet get = new HttpGet(ourServerBase + "/AllergyIntolerance?_has=Provenance:target:userID=12345");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
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());
assertThat(resp, stringContainsInOrder("THIS IS THE DESC"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -2798,7 +2781,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("Underweight"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().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.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 {
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
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.addHeader("If-Match", "W/\"9\"");
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
assertEquals(409, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
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();
@ -2883,14 +2860,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
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)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
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.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
@ -2987,11 +2958,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
pat = new Patient();
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.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();
@ -3115,7 +3086,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient actual = ourClient.read(Patient.class, new UriDt(newId.getValue()));
assertEquals(1, actual.getContained().size());
//@formatter:off
Bundle b = ourClient
.search()
.forResource("Patient")
@ -3123,7 +3093,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.prettyPrint()
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals(1, b.getEntry().size());
}
@ -3873,6 +3842,70 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
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
public void testSearchReusesResultsDisabled() {
List<IBaseResource> resources = new ArrayList<>();
@ -4995,13 +5028,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String encoded = myFhirCtx.newJsonParser().encodeResourceToString(p);
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);
try {
assertEquals(201, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response);
response.close();
}
p = new Patient();
@ -5010,13 +5043,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
encoded = myFhirCtx.newJsonParser().encodeResourceToString(p);
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);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response);
response.close();
}
}
@ -5100,8 +5133,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
HttpPut post = new HttpPut(ourServerBase + "/Patient/A2");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString);
assertEquals(400, response.getStatusLine().getStatusCode());
@ -5109,8 +5141,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
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\"",
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);
try {
MethodOutcome resp = ourClient.update().resource(input).withId("Patient/PERSON1").execute();
ourClient.update().resource(input).withId("Patient/PERSON1").execute();
} catch (InvalidRequestException e) {
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;)"));
assertEquals(400, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().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;)"));
assertEquals(400, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5189,7 +5219,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
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\"/>"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5217,7 +5247,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5241,7 +5271,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(412, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("SHALL at least contain a contact's details or a reference to an organization"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5270,7 +5300,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5298,7 +5328,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, not(containsString("warn")));
assertThat(resp, not(containsString("error")));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().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\"/>",
"</issue>"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
}
@ -5358,7 +5388,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(resp, containsString("</contains>"));
assertThat(resp, containsString("</expansion>"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().close();
response.close();
}
@ -5378,7 +5408,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
"<display value=\"Systolic blood pressure at First encounter\"/>"));
//@formatter:on
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.getEntity().getContent().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.model.entity.ResourceTable;
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.rest.gclient.IClientExecutable;
import ca.uhn.fhir.rest.gclient.IQuery;
@ -28,6 +29,7 @@ import org.springframework.test.util.AopTestUtils;
import java.util.Date;
import java.util.UUID;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@ -68,13 +70,11 @@ public class StaleSearchDeletingSvcR4Test extends BaseResourceProviderR4Test {
myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
}
//@formatter:off
IClientExecutable<IQuery<Bundle>, Bundle> search = ourClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("Everything"))
.returnBundle(Bundle.class);
//@formatter:on
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
public static void afterClassClearContext() {
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);
}
public IEmailSender getEmailSender() {
return myEmailSender;
}
}

View File

@ -39,6 +39,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
@After
public void after() throws Exception {
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
public void testSearchResourceReferenceOnlyCorrectPath() {
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.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
@ -55,6 +56,10 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Autowired
private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc;
@Autowired
private ISchedulerService mySchedulerService;
@After
public void afterUnregisterRestHookListener() {
@ -80,9 +85,6 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
}
@Autowired
private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc;
@Before
public void beforeRegisterRestHookListener() {
mySubscriptionTestUtil.registerRestHookInterceptor();
@ -98,6 +100,8 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
ourCreatedPatients.clear();
ourUpdatedPatients.clear();
ourContentTypes.clear();
mySchedulerService.logStatus();
}
private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException {

View File

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

View File

@ -109,6 +109,16 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.addIndex("IDX_VS_CONCEPT_ORDER")
.unique(true)
.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() {

View File

@ -112,6 +112,11 @@
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- Java -->
<dependency>
<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")
})
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_PROFILE_LENGTH = 200;
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)
@OptimisticLock(excluded = true)
private Collection<ResourceLink> myResourceLinksAsTarget;
@Column(name = "RES_TYPE", length = RESTYPE_LEN)
@Column(name = "RES_TYPE", length = RESTYPE_LEN, nullable = false)
@Field
@OptimisticLock(excluded = true)
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";
/**
* 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>
* 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";
/**
* 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