Merge with master

This commit is contained in:
Matti Uusitalo 2019-04-25 10:44:07 +03:00
commit b95b4cf110
397 changed files with 14748 additions and 13224 deletions

View File

@ -1,6 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---

View File

@ -0,0 +1,22 @@
---
name: Feature Request or anything else
about: Anything that is not a bug report
title: ''
labels: ''
assignees: ''
---
NOTE: Before filing a ticket, please see the following URL:
https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help
**Describe the issue**
A clear and concise description of what the feature request is. Please include details about what problem you are ultimately trying to solve (i.e. what are you building with HAPI FHIR) and how this feature would help.
**Environment (please complete the following information):**
- HAPI FHIR Version
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here.

0
.travis.yml Normal file → Executable file
View File

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.demo;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -78,16 +80,18 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
return FhirServerConfigCommon.loggingInterceptor();
}
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
return FhirServerConfigCommon.getResponseHighlighterInterceptor();
}

View File

@ -11,14 +11,12 @@ import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
@ -101,8 +99,9 @@ public class FhirServerConfigCommon {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public static IServerInterceptor loggingInterceptor() {
public static LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(
@ -114,8 +113,9 @@ public class FhirServerConfigCommon {
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
public static IServerInterceptor getResponseHighlighterInterceptor() {
public static ResponseHighlighterInterceptor getResponseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal;
}

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.demo;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -82,16 +84,18 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
return FhirServerConfigCommon.loggingInterceptor();
}
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
return FhirServerConfigCommon.getResponseHighlighterInterceptor();
}

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
@ -61,8 +62,8 @@ public class JpaServerDemo extends RestfulServer {
} else {
throw new IllegalStateException();
}
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class);
setResourceProviders(beans);
ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
registerProviders(beans.createProviders());
/*
* The system provider implements non-resource-type methods, such as
@ -76,7 +77,7 @@ public class JpaServerDemo extends RestfulServer {
} else {
throw new IllegalStateException();
}
setPlainProviders(systemProvider);
registerProviders(systemProvider);
/*
* The conformance provider exports the supported resources, search parameters, etc for
@ -108,7 +109,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/*
* Default to JSON and pretty printing

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
@ -61,8 +62,8 @@ public class JpaServerDemoDstu2 extends RestfulServer {
} else {
throw new IllegalStateException();
}
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class);
setResourceProviders(beans);
ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
registerProviders(beans.createProviders());
/*
* The system provider implements non-resource-type methods, such as
@ -76,7 +77,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
} else {
throw new IllegalStateException();
}
setPlainProviders(systemProvider);
registerProvider(systemProvider);
/*
* The conformance provider exports the supported resources, search parameters, etc for
@ -108,7 +109,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/*
* Default to JSON and pretty printing

View File

@ -183,8 +183,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

View File

@ -7,7 +7,6 @@ import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -94,8 +93,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(
@ -107,9 +107,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal;
}

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
@ -47,14 +48,14 @@ public class JpaServerDemo extends RestfulServer {
* file which is automatically generated as a part of hapi-fhir-jpaserver-base and
* contains bean definitions for a resource provider for each resource type
*/
List<IResourceProvider> beans = myAppCtx.getBean("myResourceProvidersDstu3", List.class);
setResourceProviders(beans);
ResourceProviderFactory beans = myAppCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
registerProviders(beans.createProviders());
/*
* The system provider implements non-resource-type methods, such as
* transaction, and global history.
*/
setPlainProviders(myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class));
registerProviders(myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class));
/*
* The conformance provider exports the supported resources, search parameters, etc for
@ -75,7 +76,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/*
* Default to JSON and pretty printing

View File

@ -1,5 +1,8 @@
package example;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
@ -13,6 +16,7 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.*;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -101,7 +105,8 @@ public class AuthorizationInterceptors {
@IdParam IdDt theId,
@ResourceParam Patient theResource,
@ConditionalUrlParam String theConditionalUrl,
RequestDetails theRequestDetails) {
ServletRequestDetails theRequestDetails,
IInterceptorBroadcaster theInterceptorBroadcaster) {
// If we're processing a conditional URL...
if (isNotBlank(theConditionalUrl)) {
@ -111,20 +116,25 @@ public class AuthorizationInterceptors {
// and supply the actual ID that's being updated
IdDt actual = new IdDt("Patient", "1123");
// There are a number of possible constructors for ActionRequestDetails.
// You should supply as much detail about the sub-operation as possible
IServerInterceptor.ActionRequestDetails subRequest =
new IServerInterceptor.ActionRequestDetails(theRequestDetails, actual);
// Notify the interceptors
subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE);
}
// In a real server, perhaps we would process the conditional
// request differently and follow a separate path. Either way,
// let's pretend there is some storage code here.
theResource.setId(theId.withVersion("2"));
// Notify the interceptor framework when we're about to perform an update. This is
// useful as the authorization interceptor will pick this event up and use it
// to factor into a decision about whether the operation should be allowed to proceed.
IBaseResource previousContents = theResource;
IBaseResource newContents = theResource;
HookParams params = new HookParams()
.add(IBaseResource.class, previousContents)
.add(IBaseResource.class, newContents)
.add(RequestDetails.class, theRequestDetails)
.add(ServletRequestDetails.class, theRequestDetails);
theInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, params);
MethodOutcome retVal = new MethodOutcome();
retVal.setCreated(true);
retVal.setResource(theResource);

View File

@ -33,7 +33,7 @@ public interface IRestfulClient extends IBasicClient {
/**
* The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with
* search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This
* example searches by family name.
*

View File

@ -1,16 +1,15 @@
package example;
import java.io.IOException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.DataFormatException;
@SuppressWarnings("unused")
public class Narrative {
public static void main(String[] args) throws DataFormatException, IOException {
public static void main(String[] args) throws DataFormatException {
//START SNIPPET: example1
Patient patient = new Patient();
@ -21,7 +20,7 @@ patient.addAddress().addLine("742 Evergreen Terrace").setCity("Springfield").set
FhirContext ctx = FhirContext.forDstu2();
// Use the narrative generator
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ctx));
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
// Encode the output, including the narrative
String output = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);

View File

@ -1,18 +1,17 @@
package example;
import java.io.IOException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator;
@SuppressWarnings("unused")
public class NarrativeGenerator {
public void testGenerator() throws IOException {
public void testGenerator() {
//START SNIPPET: gen
FhirContext ctx = FhirContext.forDstu2();
String propFile = "classpath:/com/foo/customnarrative.properties";
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(ctx, propFile);
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(propFile);
ctx.setNarrativeGenerator(gen);
//END SNIPPET: gen

View File

@ -0,0 +1,4 @@
package example;
public class NewInterceptors {
}

View File

@ -55,7 +55,7 @@ public class RestfulObservationResourceProvider implements IResourceProvider {
/**
* The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with
* search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This
* example searches by family name.
*

View File

@ -56,7 +56,7 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
/**
* The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with
* search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This
* example searches by family name.
*

View File

@ -234,6 +234,12 @@ public class ValidatorExamples {
return null;
}
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
// TODO: implement
return null;
}
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
// TODO: implement

View File

@ -1,8 +1,8 @@
package example.interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage;

View File

@ -139,6 +139,10 @@
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
</dependency>
<dependency>
<groupId>org.checkerframework</groupId>
<artifactId>checker-compat-qual</artifactId>
</dependency>
</ignoredDependencies>
<ignoredResourcePatterns>
<ignoredResourcePattern>changelog.txt</ignoredResourcePattern>
@ -198,7 +202,6 @@
<linksource>true</linksource>
<verbose>false</verbose>
<debug>false</debug>
<additionalparam>-Xdoclint:none</additionalparam>
<additionalJOption>-Xdoclint:none</additionalJOption>
</configuration>
<executions>

View File

@ -101,13 +101,36 @@ public class HapiLocalizer {
String formatString = getFormatString(theQualifiedKey);
format = new MessageFormat(formatString.trim());
format = newMessageFormat(formatString);
myKeyToMessageFormat.put(theQualifiedKey, format);
return format.format(theParameters);
}
return getFormatString(theQualifiedKey);
}
MessageFormat newMessageFormat(String theFormatString) {
StringBuilder pattern = new StringBuilder(theFormatString.trim());
for (int i = 0; i < (pattern.length()-1); i++) {
if (pattern.charAt(i) == '{') {
char nextChar = pattern.charAt(i+1);
if (nextChar >= '0' && nextChar <= '9') {
continue;
}
pattern.replace(i, i+1, "'{'");
int closeBraceIndex = pattern.indexOf("}", i);
if (closeBraceIndex > 0) {
i = closeBraceIndex;
pattern.replace(i, i+1, "'}'");
}
}
}
return new MessageFormat(pattern.toString());
}
protected void init() {
for (String nextName : myBundleNames) {
myBundle.add(ResourceBundle.getBundle(nextName));

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api;
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@ -27,12 +27,8 @@ import java.lang.annotation.Target;
/**
* This annotation should be placed on
* {@link Interceptor Subscription Interceptor}
* {@link Interceptor}
* bean methods.
* <p>
* Methods with this annotation are invoked immediately before a REST HOOK
* subscription delivery
* </p>
*
* @see Interceptor
*/
@ -43,6 +39,15 @@ public @interface Hook {
/**
* Provides the specific point where this method should be invoked
*/
Pointcut[] value();
Pointcut value();
/**
* The order that interceptors should be called in. Lower numbers happen before higher numbers. Default is 0
* and allowable values can be positive or negative or 0.
* <p>
* If no order is specified, or the order is set to <code>0</code> (the default order),
* the order specified at the interceptor type level will take precedence.
* </p>
*/
int order() default Interceptor.DEFAULT_ORDER;
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api;
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@ -23,10 +23,14 @@ package ca.uhn.fhir.jpa.model.interceptor.api;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class HookParams {
@ -48,24 +52,52 @@ public class HookParams {
}
@SuppressWarnings("unchecked")
private <T> void add(T theNext) {
public <T> HookParams add(@Nonnull T theNext) {
Class<T> nextClass = (Class<T>) theNext.getClass();
add(nextClass, theNext);
return this;
}
public <T> HookParams add(Class<T> theType, T theParam) {
return doAdd(theType, theParam);
}
// /**
// * This is useful for providing a lazy-loaded (generally expensive to create)
// * parameters
// */
// public <T> HookParams addSupplier(Class<T> theType, Supplier<T> theParam) {
// return doAdd(theType, theParam);
// }
private <T> HookParams doAdd(Class<T> theType, Object theParam) {
Validate.isTrue(theType.equals(Supplier.class) == false, "Can not add parameters of type Supplier");
myParams.put(theType, theParam);
return this;
}
public <T> T get(Class<T> theParamType) {
return get(theParamType, 0);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> theParamType, int theIndex) {
List<T> objects = (List<T>) myParams.get(theParamType);
T retVal = null;
List<Object> objects = myParams.get(theParamType);
Object retVal = null;
if (objects.size() > theIndex) {
retVal = objects.get(theIndex);
}
return retVal;
retVal = unwrapValue(retVal);
return (T) retVal;
}
private Object unwrapValue(Object theValue) {
if (theValue instanceof Supplier) {
theValue = ((Supplier) theValue).get();
}
return theValue;
}
/**
@ -73,10 +105,31 @@ public class HookParams {
* key is the param type and the value is the actual instance
*/
public ListMultimap<Class<?>, Object> getParamsForType() {
return Multimaps.unmodifiableListMultimap(myParams);
ArrayListMultimap<Class<?>, Object> retVal = ArrayListMultimap.create();
myParams.entries().forEach(entry -> retVal.put(entry.getKey(), unwrapValue(entry.getValue())));
return Multimaps.unmodifiableListMultimap(retVal);
}
public Collection<Object> values() {
return Collections.unmodifiableCollection(myParams.values());
return
Collections.unmodifiableCollection(myParams.values())
.stream()
.map(t -> unwrapValue(t))
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
public <T> HookParams addIfMatchesType(Class<T> theType, Object theParam) {
if (theParam == null) {
add(theType, null);
} else {
if (theType.isAssignableFrom(theParam.getClass())) {
T param = (T) theParam;
add(theType, param);
} else {
add(theType, null);
}
}
return this;
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api;
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@ -29,8 +29,8 @@ import com.google.common.annotations.VisibleForTesting;
*/
@FunctionalInterface
@VisibleForTesting
public interface IAnonymousLambdaHook {
public interface IAnonymousInterceptor {
void invoke(HookParams theArgs);
void invoke(Pointcut thePointcut, HookParams theArgs);
}

View File

@ -0,0 +1,42 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IInterceptorBroadcaster {
/**
* Invoke registered interceptor hook methods for the given Pointcut.
*
* @return Returns <code>false</code> if any of the invoked hook methods returned
* <code>false</code>, and returns <code>true</code> otherwise.
*/
boolean callHooks(Pointcut thePointcut, HookParams theParams);
/**
* Invoke registered interceptor hook methods for the given Pointcut. This method
* should only be called for pointcuts that return a type other than
* <code>void</code> or <code>boolean</code>
*
* @return Returns the object returned by the first hook method that did not return <code>null</code>
*/
Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams);
}

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.interceptor.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 javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
public interface IInterceptorService extends IInterceptorBroadcaster {
/**
* Register an interceptor that will be used in a {@link ThreadLocal} context.
* This means that events will only be broadcast to the given interceptor if
* they were fired from the current thread.
* <p>
* Note that it is almost always desirable to call this method with a
* try-finally statement that removes the interceptor afterwards, since
* this can lead to memory leakage, poor performance due to ever-increasing
* numbers of interceptors, etc.
* </p>
* <p>
* Note that most methods such as {@link #getAllRegisteredInterceptors()} and
* {@link #unregisterAllInterceptors()} do not affect thread local interceptors
* as they are kept in a separate list.
* </p>
*
* @param theInterceptor The interceptor
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerThreadLocalInterceptor(Object theInterceptor);
/**
* Unregisters a ThreadLocal interceptor
*
* @param theInterceptor The interceptor
* @see #registerThreadLocalInterceptor(Object)
*/
void unregisterThreadLocalInterceptor(Object theInterceptor);
/**
* Register an interceptor. This method has no effect if the given interceptor is already registered.
*
* @param theInterceptor The interceptor to register
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerInterceptor(Object theInterceptor);
/**
* Unregister an interceptor. This method has no effect if the given interceptor is not already registered.
*
* @param theInterceptor The interceptor to unregister
*/
void unregisterInterceptor(Object theInterceptor);
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor);
/**
* Returns all currently registered interceptors (excluding any thread local interceptors).
*/
List<Object> getAllRegisteredInterceptors();
/**
* Unregisters all registered interceptors. Note that this method does not unregister
* any {@link #registerThreadLocalInterceptor(Object) thread local interceptors}.
*/
void unregisterAllInterceptors();
void unregisterInterceptors(@Nullable Collection<?> theInterceptors);
void registerInterceptors(@Nullable Collection<?> theInterceptors);
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api;
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@ -33,9 +33,13 @@ import java.lang.annotation.Target;
public @interface Interceptor {
/**
* @return Declares that an interceptor should be manually registered with the registry,
* and should not auto-register using Spring autowiring.
* @see #order()
*/
boolean manualRegistration() default false;
int DEFAULT_ORDER = 0;
/**
* The order that interceptors should be called in. Lower numbers happen before higher numbers. Default is 0
* and allowable values can be positive or negative or 0.
*/
int order() default DEFAULT_ORDER;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,579 @@
package ca.uhn.fhir.interceptor.executor;
/*-
* #%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 ca.uhn.fhir.interceptor.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster {
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class);
private final List<Object> myInterceptors = new ArrayList<>();
private final ListMultimap<Pointcut, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
private final Object myRegistryMutex = new Object();
private final ThreadLocal<ListMultimap<Pointcut, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>();
private String myName;
private boolean myThreadlocalInvokersEnabled = true;
/**
* Constructor which uses a default name of "default"
*/
public InterceptorService() {
this("default");
}
/**
* Constructor
*
* @param theName The name for this registry (useful for troubleshooting)
*/
public InterceptorService(String theName) {
super();
myName = theName;
}
/**
* Are threadlocal interceptors enabled on this registry (defaults to true)
*/
public boolean isThreadlocalInvokersEnabled() {
return myThreadlocalInvokersEnabled;
}
/**
* Are threadlocal interceptors enabled on this registry (defaults to true)
*/
public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) {
myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled;
}
@VisibleForTesting
List<Object> getGlobalInterceptorsForUnitTest() {
return myInterceptors;
}
@Override
@VisibleForTesting
public void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor) {
registerAnonymousInterceptor(thePointcut, Interceptor.DEFAULT_ORDER, theInterceptor);
}
public void setName(String theName) {
myName = theName;
}
@Override
public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) {
Validate.notNull(thePointcut);
Validate.notNull(theInterceptor);
synchronized (myRegistryMutex) {
myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder));
if (!isInterceptorAlreadyRegistered(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
}
@Override
public List<Object> getAllRegisteredInterceptors() {
synchronized (myRegistryMutex) {
List<Object> retVal = new ArrayList<>();
retVal.addAll(myInterceptors);
return Collections.unmodifiableList(retVal);
}
}
@Override
@VisibleForTesting
public void unregisterAllInterceptors() {
synchronized (myRegistryMutex) {
myAnonymousInvokers.clear();
myGlobalInvokers.clear();
myInterceptors.clear();
}
}
@Override
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
if (theInterceptors != null) {
theInterceptors.forEach(t -> unregisterInterceptor(t));
}
}
@Override
public void registerInterceptors(@Nullable Collection<?> theInterceptors) {
if (theInterceptors != null) {
theInterceptors.forEach(t -> registerInterceptor(t));
}
}
@Override
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
if (!myThreadlocalInvokersEnabled) {
return false;
}
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers);
return !invokers.isEmpty();
}
@Override
public void unregisterThreadLocalInterceptor(Object theInterceptor) {
if (myThreadlocalInvokersEnabled) {
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
if (invokers.isEmpty()) {
myThreadlocalInvokers.remove();
}
}
}
private ListMultimap<Pointcut, BaseInvoker> getThreadLocalInvokerMultimap() {
ListMultimap<Pointcut, BaseInvoker> invokers = myThreadlocalInvokers.get();
if (invokers == null) {
invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
myThreadlocalInvokers.set(invokers);
}
return invokers;
}
@Override
public boolean registerInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
if (isInterceptorAlreadyRegistered(theInterceptor)) {
return false;
}
List<HookInvoker> addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers);
if (addedInvokers.isEmpty()) {
ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName());
return false;
}
// Add to the global list
myInterceptors.add(theInterceptor);
sortByOrderAnnotation(myInterceptors);
return true;
}
}
private boolean isInterceptorAlreadyRegistered(Object theInterceptor) {
for (Object next : myInterceptors) {
if (next == theInterceptor) {
return true;
}
}
return false;
}
@Override
public void unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
myInterceptors.removeIf(t -> t == theInterceptor);
myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
}
}
private void sortByOrderAnnotation(List<Object> theObjects) {
IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>();
for (Object next : theObjects) {
Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class);
int order = orderAnnotation != null ? orderAnnotation.order() : 0;
interceptorToOrder.put(next, order);
}
theObjects.sort((a, b) -> {
Integer orderA = interceptorToOrder.get(a);
Integer orderB = interceptorToOrder.get(b);
return orderA - orderB;
});
}
@Override
public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams);
assert thePointcut.getReturnType() != void.class && thePointcut.getReturnType() != boolean.class;
Object retVal = doCallHooks(thePointcut, theParams, null);
return retVal;
}
@Override
public boolean callHooks(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams);
assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class;
Object retValObj = doCallHooks(thePointcut, theParams, true);
return (Boolean) retValObj;
}
private Object doCallHooks(Pointcut thePointcut, HookParams theParams, Object theRetVal) {
List<BaseInvoker> invokers = getInvokersForPointcut(thePointcut);
/*
* Call each hook in order
*/
for (BaseInvoker nextInvoker : invokers) {
Object nextOutcome = nextInvoker.invoke(theParams);
if (thePointcut.getReturnType() == boolean.class) {
Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome;
if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) {
ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker);
theRetVal = false;
break;
}
} else if (thePointcut.getReturnType() != void.class) {
if (nextOutcome != null) {
theRetVal = nextOutcome;
break;
}
}
}
return theRetVal;
}
@VisibleForTesting
List<Object> getInterceptorsWithInvokersForPointcut(Pointcut thePointcut) {
return getInvokersForPointcut(thePointcut)
.stream()
.map(BaseInvoker::getInterceptor)
.collect(Collectors.toList());
}
/**
* Returns an ordered list of invokers for the given pointcut. Note that
* a new and stable list is returned to.. do whatever you want with it.
*/
private List<BaseInvoker> getInvokersForPointcut(Pointcut thePointcut) {
List<BaseInvoker> invokers;
synchronized (myRegistryMutex) {
List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut);
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
List<BaseInvoker> threadLocalInvokers = null;
if (myThreadlocalInvokersEnabled) {
ListMultimap<Pointcut, BaseInvoker> pointcutToInvokers = myThreadlocalInvokers.get();
if (pointcutToInvokers != null) {
threadLocalInvokers = pointcutToInvokers.get(thePointcut);
}
}
invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers);
}
return invokers;
}
/**
* First argument must be the global invoker list!!
*/
@SafeVarargs
private final List<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) {
List<BaseInvoker> haveOne = null;
boolean haveMultiple = false;
for (List<BaseInvoker> nextInvokerList : theInvokersLists) {
if (nextInvokerList == null || nextInvokerList.isEmpty()) {
continue;
}
if (haveOne == null) {
haveOne = nextInvokerList;
} else {
haveMultiple = true;
}
}
if (haveOne == null) {
return Collections.emptyList();
}
List<BaseInvoker> retVal;
if (haveMultiple == false) {
// The global list doesn't need to be sorted every time since it's sorted on
// insertion each time. Doing so is a waste of cycles..
if (haveOne == theInvokersLists[0]) {
retVal = haveOne;
} else {
retVal = new ArrayList<>(haveOne);
retVal.sort(Comparator.naturalOrder());
}
} else {
retVal = Arrays
.stream(theInvokersLists)
.filter(t -> t != null)
.flatMap(t -> t.stream())
.sorted()
.collect(Collectors.toList());
}
return retVal;
}
/**
* Only call this when assertions are enabled, it's expensive
*/
boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) {
Validate.isTrue(theParams.getParamsForType().values().size() == thePointcut.getParameterTypes().size(), "Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t != null ? t.getClass().getSimpleName() : "null").sorted().collect(Collectors.toList()));
List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
for (Class<?> nextTypeClass : givenTypes.keySet()) {
String nextTypeName = nextTypeClass.getName();
for (Object nextParamValue : givenTypes.get(nextTypeClass)) {
Validate.isTrue(nextParamValue == null || nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue != null ? nextParamValue.getClass() : "null", nextTypeClass);
Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName);
}
}
return true;
}
private class AnonymousLambdaInvoker extends BaseInvoker {
private final IAnonymousInterceptor myHook;
private final Pointcut myPointcut;
public AnonymousLambdaInvoker(Pointcut thePointcut, IAnonymousInterceptor theHook, int theOrder) {
super(theHook, theOrder);
myHook = theHook;
myPointcut = thePointcut;
}
@Override
Object invoke(HookParams theParams) {
myHook.invoke(myPointcut, theParams);
return true;
}
}
private abstract static class BaseInvoker implements Comparable<BaseInvoker> {
private final int myOrder;
private final Object myInterceptor;
BaseInvoker(Object theInterceptor, int theOrder) {
myInterceptor = theInterceptor;
myOrder = theOrder;
}
public Object getInterceptor() {
return myInterceptor;
}
abstract Object invoke(HookParams theParams);
@Override
public int compareTo(BaseInvoker theInvoker) {
return myOrder - theInvoker.myOrder;
}
}
private static class HookInvoker extends BaseInvoker {
private final Method myMethod;
private final Class<?>[] myParameterTypes;
private final int[] myParameterIndexes;
private final Pointcut myPointcut;
/**
* Constructor
*/
private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
super(theInterceptor, theOrder);
myPointcut = theHook.value();
myParameterTypes = theHookMethod.getParameterTypes();
myMethod = theHookMethod;
Class<?> returnType = theHookMethod.getReturnType();
if (myPointcut.getReturnType().equals(boolean.class)) {
Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod);
} else if (myPointcut.getReturnType().equals(void.class)) {
Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod);
} else {
Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod);
}
myParameterIndexes = new int[myParameterTypes.length];
Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>();
for (int i = 0; i < myParameterTypes.length; i++) {
AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0));
myParameterIndexes[i] = counter.getAndIncrement();
}
myMethod.setAccessible(true);
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("method", myMethod)
.toString();
}
public Pointcut getPointcut() {
return myPointcut;
}
/**
* @return Returns true/false if the hook method returns a boolean, returns true otherwise
*/
@Override
Object invoke(HookParams theParams) {
Object[] args = new Object[myParameterTypes.length];
for (int i = 0; i < myParameterTypes.length; i++) {
Class<?> nextParamType = myParameterTypes[i];
int nextParamIndex = myParameterIndexes[i];
Object nextParamValue = theParams.get(nextParamType, nextParamIndex);
args[i] = nextParamValue;
}
// Invoke the method
try {
return myMethod.invoke(getInterceptor(), args);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (myPointcut.isShouldLogAndSwallowException(targetException)) {
ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException);
return null;
}
if (targetException instanceof RuntimeException) {
throw ((RuntimeException) targetException);
} else {
throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException);
}
} catch (Exception e) {
throw new InternalErrorException(e);
}
}
}
private static List<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<Pointcut, BaseInvoker> theInvokers) {
Class<?> interceptorClass = theInterceptor.getClass();
int typeOrder = determineOrder(interceptorClass);
List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
// Invoke the REGISTERED pointcut for any added hooks
addedInvokers.stream()
.filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut()))
.forEach(t -> t.invoke(new HookParams()));
// Register the interceptor and its various hooks
for (HookInvoker nextAddedHook : addedInvokers) {
Pointcut nextPointcut = nextAddedHook.getPointcut();
if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) {
continue;
}
theInvokers.put(nextPointcut, nextAddedHook);
}
// Make sure we're always sorted according to the order declared in
// @Order
for (Pointcut nextPointcut : theInvokers.keys()) {
List<BaseInvoker> nextInvokerList = theInvokers.get(nextPointcut);
nextInvokerList.sort(Comparator.naturalOrder());
}
return addedInvokers;
}
/**
* @return Returns a list of any added invokers
*/
private static List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
ArrayList<HookInvoker> retVal = new ArrayList<>();
for (Method nextMethod : theInterceptor.getClass().getMethods()) {
Optional<Hook> hook = findAnnotation(nextMethod, Hook.class);
if (hook.isPresent()) {
int methodOrder = theTypeOrder;
int methodOrderAnnotation = hook.get().order();
if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) {
methodOrder = methodOrderAnnotation;
}
retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder));
}
}
return retVal;
}
private static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> theHookClass) {
T annotation;
if (theObject instanceof Method) {
annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true);
} else {
annotation = theObject.getAnnotation(theHookClass);
}
return Optional.ofNullable(annotation);
}
private static int determineOrder(Class<?> theInterceptorClass) {
int typeOrder = Interceptor.DEFAULT_ORDER;
Optional<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
if (typeOrderAnnotation.isPresent()) {
typeOrder = typeOrderAnnotation.get().order();
}
return typeOrder;
}
private static String toErrorString(List<String> theParameterTypes) {
return theParameterTypes
.stream()
.sorted()
.collect(Collectors.joining(","));
}
}

View File

@ -29,8 +29,8 @@ import ca.uhn.fhir.rest.api.QualifiedParamList;
public interface IQueryParameterOr<T extends IQueryParameterType> extends Serializable {
public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, QualifiedParamList theParameters);
void setValuesAsQueryTokens(FhirContext theContext, String theParamName, QualifiedParamList theParameters);
public List<T> getValuesAsQueryTokens();
List<T> getValuesAsQueryTokens();
}

View File

@ -36,16 +36,16 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative
/**
* Constructor
*/
public BaseThymeleafNarrativeGenerator(FhirContext theFhirContext) {
super(theFhirContext);
protected BaseThymeleafNarrativeGenerator() {
super();
}
@Override
public boolean populateResourceNarrative(IBaseResource theResource) {
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
if (!myInitialized) {
initialize();
}
super.populateResourceNarrative(theResource);
super.populateResourceNarrative(theFhirContext, theResource);
return false;
}
@ -58,7 +58,7 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative
List<String> propFileName = getPropertyFile();
try {
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(getFhirContext(), propFileName);
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(propFileName);
setManifest(manifest);
} catch (IOException e) {
throw new InternalErrorException(e);

View File

@ -23,7 +23,6 @@ package ca.uhn.fhir.narrative;
import java.util.Arrays;
import java.util.List;
import ca.uhn.fhir.context.FhirContext;
import org.apache.commons.lang3.Validate;
public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator {
@ -40,8 +39,8 @@ public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGen
* <li>classpath:/com/package/file.properties</li>
* </ul>
*/
public CustomThymeleafNarrativeGenerator(FhirContext theFhirContext, String... thePropertyFile) {
super(theFhirContext);
public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
super();
setPropertyFile(thePropertyFile);
}

View File

@ -20,8 +20,6 @@ package ca.uhn.fhir.narrative;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import java.util.ArrayList;
import java.util.List;
@ -32,8 +30,8 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
private boolean myUseHapiServerConformanceNarrative;
public DefaultThymeleafNarrativeGenerator(FhirContext theFhirContext) {
super(theFhirContext);
public DefaultThymeleafNarrativeGenerator() {
super();
}
@Override

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.narrative;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface INarrativeGenerator {
@ -32,6 +33,6 @@ public interface INarrativeGenerator {
*
* @return Returns <code>true</code> if a narrative was actually generated
*/
boolean populateResourceNarrative(IBaseResource theResource);
boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource);
}

View File

@ -27,14 +27,13 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.INarrative;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -44,12 +43,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
private INarrativeTemplateManifest myManifest;
private final FhirContext myFhirContext;
public BaseNarrativeGenerator(FhirContext theFhirContext) {
Validate.notNull(theFhirContext, "theFhirContext must not be null");
myFhirContext = theFhirContext;
}
public INarrativeTemplateManifest getManifest() {
return myManifest;
@ -59,43 +52,40 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
myManifest = theManifest;
}
public FhirContext getFhirContext() {
return myFhirContext;
}
@Override
public boolean populateResourceNarrative(IBaseResource theResource) {
Optional<INarrativeTemplate> templateOpt = getTemplateForElement(theResource);
if (templateOpt.isPresent()) {
return applyTemplate(templateOpt.get(), theResource);
} else {
return false;
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
List<INarrativeTemplate> templateOpt = getTemplateForElement(theFhirContext, theResource);
if (templateOpt.size() > 0) {
applyTemplate(theFhirContext, templateOpt.get(0), theResource);
return true;
}
return false;
}
private Optional<INarrativeTemplate> getTemplateForElement(IBase theElement) {
return myManifest.getTemplateByElement(getStyle(), theElement);
private List<INarrativeTemplate> getTemplateForElement(FhirContext theFhirContext, IBase theElement) {
return myManifest.getTemplateByElement(theFhirContext, getStyle(), theElement);
}
private boolean applyTemplate(INarrativeTemplate theTemplate, IBaseResource theResource) {
private boolean applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) {
if (templateDoesntApplyToResource(theTemplate, theResource)) {
return false;
}
boolean retVal = false;
String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
String resourceName = theFhirContext.getResourceDefinition(theResource).getName();
String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName);
// Narrative templates define a path within the resource that they apply to. Here, we're
// finding anywhere in the resource that gets a narrative
List<IBase> targets = findElementsInResourceRequiringNarratives(theResource, contextPath);
List<IBase> targets = findElementsInResourceRequiringNarratives(theFhirContext, theResource, contextPath);
for (IBase nextTargetContext : targets) {
// Extract [element].text of type Narrative
INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(nextTargetContext);
INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(theFhirContext, nextTargetContext);
// Create the actual narrative text
String narrative = applyTemplate(theTemplate, nextTargetContext);
String narrative = applyTemplate(theFhirContext, theTemplate, nextTargetContext);
narrative = cleanWhitespace(narrative);
if (isNotBlank(narrative)) {
@ -112,13 +102,13 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return retVal;
}
private INarrative getOrCreateNarrativeChildElement(IBase nextTargetContext) {
BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) getFhirContext().getElementDefinition(nextTargetContext.getClass());
private INarrative getOrCreateNarrativeChildElement(FhirContext theFhirContext, IBase nextTargetContext) {
BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(nextTargetContext.getClass());
BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text");
List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext);
INarrative nextTargetNarrative;
if (existing.isEmpty()) {
nextTargetNarrative = (INarrative) getFhirContext().getElementDefinition("narrative").newInstance();
nextTargetNarrative = (INarrative) theFhirContext.getElementDefinition("narrative").newInstance();
targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative);
} else {
nextTargetNarrative = (INarrative) existing.get(0);
@ -126,15 +116,15 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return nextTargetNarrative;
}
private List<IBase> findElementsInResourceRequiringNarratives(IBaseResource theResource, String theContextPath) {
if (myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
private List<IBase> findElementsInResourceRequiringNarratives(FhirContext theFhirContext, IBaseResource theResource, String theContextPath) {
if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
return Collections.singletonList(theResource);
}
IFluentPath fhirPath = myFhirContext.newFluentPath();
IFluentPath fhirPath = theFhirContext.newFluentPath();
return fhirPath.evaluate(theResource, theContextPath, IBase.class);
}
protected abstract String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext);
protected abstract String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext);
private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) {
boolean retVal = false;
@ -156,7 +146,7 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return retVal;
}
protected abstract TemplateTypeEnum getStyle();
protected abstract EnumSet<TemplateTypeEnum> getStyle();
/**
* Trims the superfluous whitespace out of an HTML block
@ -188,7 +178,6 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) {
b.append(' ');
}
inWhitespace = false;
b.append(nextChar);
inWhitespace = false;
betweenTags = false;

View File

@ -20,14 +20,16 @@ package ca.uhn.fhir.narrative2;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase;
import java.util.Optional;
import java.util.EnumSet;
import java.util.List;
public interface INarrativeTemplateManifest {
Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName);
List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName);
Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName);
List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName);
Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElementValue);
List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElementValue);
}

View File

@ -34,41 +34,37 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToResourceTypeToTemplate;
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToDatatypeToTemplate;
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToNameToTemplate;
private final FhirContext myCtx;
private final Map<String, List<NarrativeTemplate>> myStyleToResourceTypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myStyleToDatatypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myStyleToNameToTemplate;
private final int myTemplateCount;
private NarrativeTemplateManifest(FhirContext theFhirContext, Collection<NarrativeTemplate> theTemplates) {
myCtx = theFhirContext;
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToResourceTypeToTemplate = new HashMap<>();
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToDatatypeToTemplate = new HashMap<>();
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToNameToTemplate = new HashMap<>();
private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) {
Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>();
for (NarrativeTemplate nextTemplate : theTemplates) {
Map<String, NarrativeTemplate> resourceTypeToTemplate = styleToResourceTypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
Map<String, NarrativeTemplate> datatypeToTemplate = styleToDatatypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
Map<String, NarrativeTemplate> nameToTemplate = styleToNameToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate);
nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate);
for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) {
resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate);
resourceTypeToTemplate.computeIfAbsent(nextResourceType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
}
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate);
datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
}
}
myTemplateCount = theTemplates.size();
myStyleToNameToTemplate = makeImmutable(styleToNameToTemplate);
myStyleToResourceTypeToTemplate = makeImmutable(styleToResourceTypeToTemplate);
myStyleToDatatypeToTemplate = makeImmutable(styleToDatatypeToTemplate);
myStyleToNameToTemplate = makeImmutable(nameToTemplate);
myStyleToResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
myStyleToDatatypeToTemplate = makeImmutable(datatypeToTemplate);
}
public int getNamedTemplateCount() {
@ -76,31 +72,31 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
}
@Override
public Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName) {
return getFromMap(theStyle, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
public List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) {
return getFromMap(theStyles, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
}
@Override
public Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName) {
return getFromMap(theStyle, theName, myStyleToNameToTemplate);
public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) {
return getFromMap(theStyles, theName, myStyleToNameToTemplate);
}
@Override
public Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElement) {
public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) {
if (theElement instanceof IBaseResource) {
String resourceName = myCtx.getResourceDefinition((IBaseResource) theElement).getName();
return getTemplateByResourceName(theStyle, resourceName);
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
return getTemplateByResourceName(theFhirContext, theStyles, resourceName);
} else {
String datatypeName = myCtx.getElementDefinition(theElement.getClass()).getName();
return getFromMap(theStyle, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
return getFromMap(theStyles, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
}
}
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, String... thePropertyFilePaths) throws IOException {
return forManifestFileLocation(theFhirContext, Arrays.asList(thePropertyFilePaths));
public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
return forManifestFileLocation(Arrays.asList(thePropertyFilePaths));
}
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, Collection<String> thePropertyFilePaths) throws IOException {
public static NarrativeTemplateManifest forManifestFileLocation(Collection<String> thePropertyFilePaths) throws IOException {
ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size());
@ -109,19 +105,19 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
manifestFileContents.add(resource);
}
return forManifestFileContents(theFhirContext, manifestFileContents);
return forManifestFileContents(manifestFileContents);
}
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, String... theResources) throws IOException {
return forManifestFileContents(theFhirContext, Arrays.asList(theResources));
public static NarrativeTemplateManifest forManifestFileContents(String... theResources) throws IOException {
return forManifestFileContents(Arrays.asList(theResources));
}
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, Collection<String> theResources) throws IOException {
public static NarrativeTemplateManifest forManifestFileContents(Collection<String> theResources) throws IOException {
List<NarrativeTemplate> templates = new ArrayList<>();
for (String next : theResources) {
templates.addAll(loadProperties(next));
}
return new NarrativeTemplateManifest(theFhirContext, templates);
return new NarrativeTemplateManifest(templates);
}
private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException {
@ -222,17 +218,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
}
}
private static <T> Optional<INarrativeTemplate> getFromMap(TemplateTypeEnum theStyle, T theResourceName, Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theMap) {
NarrativeTemplate retVal = null;
Map<T, NarrativeTemplate> resourceTypeToTemplate = theMap.get(theStyle);
if (resourceTypeToTemplate != null) {
retVal = resourceTypeToTemplate.get(theResourceName);
}
return Optional.ofNullable(retVal);
private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
return theMap
.getOrDefault(theKey, Collections.emptyList())
.stream()
.filter(t->theStyles.contains(t.getTemplateType()))
.collect(Collectors.toList());
}
private static <T> Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> makeImmutable(Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableMap(value));
private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value));
return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
}

View File

@ -20,12 +20,13 @@ package ca.uhn.fhir.narrative2;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import org.hl7.fhir.instance.model.api.IBaseResource;
public class NullNarrativeGenerator implements INarrativeGenerator {
@Override
public boolean populateResourceNarrative(IBaseResource theResource) {
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
return false;
}
}

View File

@ -45,9 +45,7 @@ import org.thymeleaf.templateresolver.DefaultTemplateResolver;
import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.templateresource.StringTemplateResource;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -58,13 +56,13 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
/**
* Constructor
*/
public ThymeleafNarrativeGenerator(FhirContext theFhirContext) {
super(theFhirContext);
public ThymeleafNarrativeGenerator() {
super();
}
private TemplateEngine getTemplateEngine() {
private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
TemplateEngine engine = new TemplateEngine();
ProfileResourceResolver resolver = new ProfileResourceResolver();
ProfileResourceResolver resolver = new ProfileResourceResolver(theFhirContext);
engine.setTemplateResolver(resolver);
if (myMessageResolver != null) {
engine.setMessageResolver(myMessageResolver);
@ -73,8 +71,8 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
@Override
public Set<IProcessor> getProcessors(String theDialectPrefix) {
Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
retVal.add(new NarrativeTagProcessor(theDialectPrefix));
retVal.add(new NarrativeAttributeProcessor(theDialectPrefix));
retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
return retVal;
}
@ -85,25 +83,23 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
}
@Override
protected String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext) {
protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
Context context = new Context();
context.setVariable("resource", theTargetContext);
context.setVariable("context", theTargetContext);
context.setVariable("fhirVersion", getFhirContext().getVersion().getVersion().name());
context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name());
String result = getTemplateEngine().process(theTemplate.getTemplateName(), context);
return result;
return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
}
@Override
protected TemplateTypeEnum getStyle() {
return TemplateTypeEnum.THYMELEAF;
protected EnumSet<TemplateTypeEnum> getStyle() {
return EnumSet.of(TemplateTypeEnum.THYMELEAF);
}
private String applyTemplateWithinTag(ITemplateContext theTemplateContext, String theName, String theElement) {
private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
IEngineConfiguration configuration = theTemplateContext.getConfiguration();
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
@ -113,20 +109,20 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
return "";
}
Optional<INarrativeTemplate> templateOpt;
List<INarrativeTemplate> templateOpt;
if (isNotBlank(theName)) {
templateOpt = getManifest().getTemplateByName(getStyle(), theName);
if (!templateOpt.isPresent()) {
templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
if (templateOpt.isEmpty()) {
throw new InternalErrorException("Unknown template name: " + theName);
}
} else {
templateOpt = getManifest().getTemplateByElement(getStyle(), elementValue);
if (!templateOpt.isPresent()) {
templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
if (templateOpt.isEmpty()) {
throw new InternalErrorException("No template for type: " + elementValue.getClass());
}
}
return applyTemplate(templateOpt.get(), elementValue);
return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
}
public void setMessageResolver(IMessageResolver theMessageResolver) {
@ -135,9 +131,15 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
private class ProfileResourceResolver extends DefaultTemplateResolver {
private final FhirContext myFhirContext;
private ProfileResourceResolver(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override
protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return getManifest().getTemplateByName(getStyle(), theTemplate).isPresent();
return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
}
@Override
@ -148,7 +150,9 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
@Override
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return getManifest()
.getTemplateByName(getStyle(), theTemplate)
.getTemplateByName(myFhirContext, getStyle(), theTemplate)
.stream()
.findFirst()
.map(t -> new StringTemplateResource(t.getTemplateText()))
.orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
}
@ -161,8 +165,11 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
private class NarrativeTagProcessor extends AbstractElementTagProcessor {
public NarrativeTagProcessor(String dialectPrefix) {
private final FhirContext myFhirContext;
NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
myFhirContext = theFhirContext;
}
@Override
@ -170,7 +177,7 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
String name = theTag.getAttributeValue("th:name");
String element = theTag.getAttributeValue("th:element");
String appliedTemplate = applyTemplateWithinTag(theTemplateContext, name, element);
String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
theStructureHandler.replaceWith(appliedTemplate, false);
}
}
@ -181,13 +188,16 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
*/
private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
protected NarrativeAttributeProcessor(String theDialectPrefix) {
private final FhirContext myFhirContext;
NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
myFhirContext = theFhirContext;
}
@Override
protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
String text = applyTemplateWithinTag(theContext, null, theAttributeValue);
String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
theStructureHandler.setBody(text, false);
}

View File

@ -360,7 +360,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
narr = null;
}
if (narr != null && narr.isEmpty()) {
gen.populateResourceNarrative(theResource);
gen.populateResourceNarrative(myContext, theResource);
if (!narr.isEmpty()) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
String childName = nextChild.getChildNameByDatatype(child.getDatatype());

View File

@ -371,7 +371,7 @@ public class XmlParser extends BaseParser /* implements IParser */ {
}
// FIXME potential null access on narr see line 623
if (gen != null && narr.isEmpty()) {
gen.populateResourceNarrative(theResource);
gen.populateResourceNarrative(myContext, theResource);
}
if (narr != null && narr.isEmpty() == false) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;

View File

@ -19,10 +19,14 @@ package ca.uhn.fhir.rest.annotation;
* limitations under the License.
* #L%
*/
import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes a parameter for a REST method which will contain the resource actually
* Denotes a parameter for a REST method which will contain the resource actually
* being created/updated/etc in an operation which contains a resource in the HTTP request.
* <p>
* For example, in a {@link Create} operation the method parameter annotated with this
@ -32,7 +36,7 @@ import java.lang.annotation.*;
* Parameters with this annotation should typically be of the type of resource being
* operated on (see below for an exception when raw data is required). For example, in a
* IResourceProvider for Patient resources, the parameter annotated with this
* annotation should be of type Patient.
* annotation should be of type Patient.
* </p>
* <p>
* Note that in servers it is also acceptable to have parameters with this annotation
@ -41,8 +45,11 @@ import java.lang.annotation.*;
* have multiple parameters with this annotation, so you can have one parameter
* which accepts the parsed resource, and another which accepts the raw request.
* </p>
* <p>
* Also note that this parameter may be null if a client does not supply a body.
* </p>
*/
@Target(value=ElementType.PARAMETER)
@Target(value = ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceParam {

View File

@ -20,6 +20,9 @@ package ca.uhn.fhir.rest.client.api;
* #L%
*/
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import java.io.IOException;
/**
@ -35,11 +38,13 @@ public interface IClientInterceptor {
/**
* Fired by the client just before invoking the HTTP client request
*/
@Hook(Pointcut.CLIENT_REQUEST)
void interceptRequest(IHttpRequest theRequest);
/**
* Fired by the client upon receiving an HTTP response, prior to processing that response
*/
@Hook(Pointcut.CLIENT_RESPONSE)
void interceptResponse(IHttpResponse theResponse) throws IOException;
}

View File

@ -1,12 +1,13 @@
package ca.uhn.fhir.rest.client.api;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
import javax.annotation.Nonnull;
/*
* #%L
@ -30,6 +31,20 @@ import java.util.List;
public interface IRestfulClient {
/**
* Sets the interfceptor service used by this client
*
* @since 3.8.0
*/
IInterceptorService getInterceptorService();
/**
* Sets the interfceptor service used by this client
*
* @since 3.8.0
*/
void setInterceptorService(@Nonnull IInterceptorService theInterceptorService);
/**
* Retrieve the contents at the given URL and parse them as a resource. This
* method could be used as a low level implementation of a read/vread/search
@ -69,11 +84,6 @@ public interface IRestfulClient {
*/
IHttpClient getHttpClient();
/**
* Returns the client interceptors that have been registered with this client
*/
List<IClientInterceptor> getInterceptors();
/**
* Base URL for the server, with no trailing "/"
*/
@ -82,7 +92,10 @@ public interface IRestfulClient {
/**
* Register a new interceptor for this client. An interceptor can be used to add additional
* logging, or add security headers, or pre-process responses, etc.
*
* @deprecated Use {@link #getInterceptorService()} to access the list of inteerceptors, register them, and unregister them
*/
@Deprecated
void registerInterceptor(IClientInterceptor theInterceptor);
/**
@ -102,7 +115,10 @@ public interface IRestfulClient {
/**
* Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)}
*
* @deprecated Use {@link #getInterceptorService()} to access the list of inteerceptors, register them, and unregister them
*/
@Deprecated
void unregisterInterceptor(IClientInterceptor theInterceptor);
/**

View File

@ -23,30 +23,27 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.api.annotation.SimpleSetter;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.*;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQueryParameterType , */IQueryParameterOr<DateParam> {
private static final long serialVersionUID = 1L;
private final DateParamDateTimeHolder myValue = new DateParamDateTimeHolder();
/**
@ -119,9 +116,7 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
b.append(ParameterUtil.escapeWithDefault(getPrefix().getValue()));
}
if (myValue != null) {
b.append(ParameterUtil.escapeWithDefault(myValue.getValueAsString()));
}
b.append(ParameterUtil.escapeWithDefault(myValue.getValueAsString()));
return b.toString();
}
@ -132,38 +127,15 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
}
public TemporalPrecisionEnum getPrecision() {
if (myValue != null) {
return myValue.getPrecision();
}
return null;
}
public Date getValue() {
if (myValue != null) {
return myValue.getValue();
}
return null;
}
public DateTimeDt getValueAsDateTimeDt() {
if (myValue == null) {
return null;
}
return new DateTimeDt(myValue.getValue());
}
public InstantDt getValueAsInstantDt() {
if (myValue == null) {
return null;
}
return new InstantDt(myValue.getValue());
}
public String getValueAsString() {
if (myValue != null) {
return myValue.getValueAsString();
}
return null;
}
@Override
@ -260,6 +232,8 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
/**
* Constructor
*/
// LEAVE THIS AS PUBLIC!!
@SuppressWarnings("WeakerAccess")
public DateParamDateTimeHolder() {
super();
}

View File

@ -33,8 +33,8 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class HasParam extends BaseParam implements IQueryParameterType {
private static final long serialVersionUID = 1L;
private String myOwningFieldName;
private String myReferenceFieldName;
private String myParameterName;
private String myParameterValue;
private String myTargetResourceType;
@ -44,10 +44,10 @@ public class HasParam extends BaseParam implements IQueryParameterType {
}
public HasParam(String theTargetResourceType, String theOwningFieldName, String theParameterName, String theParameterValue) {
public HasParam(String theTargetResourceType, String theReferenceFieldName, String theParameterName, String theParameterValue) {
this();
myTargetResourceType = theTargetResourceType;
myOwningFieldName = theOwningFieldName;
myReferenceFieldName = theReferenceFieldName;
myParameterName = theParameterName;
myParameterValue = theParameterValue;
}
@ -75,13 +75,13 @@ public class HasParam extends BaseParam implements IQueryParameterType {
validateColon(qualifier, colonIndex1);
myTargetResourceType = qualifier.substring(1, colonIndex0);
myOwningFieldName = qualifier.substring(colonIndex0 + 1, colonIndex1);
myReferenceFieldName = qualifier.substring(colonIndex0 + 1, colonIndex1);
myParameterName = qualifier.substring(colonIndex1 + 1);
myParameterValue = theValue;
}
public String getOwningFieldName() {
return myOwningFieldName;
public String getReferenceFieldName() {
return myReferenceFieldName;
}
public String getParameterName() {

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.util.CoverageIgnore;
/*
@ -30,11 +31,14 @@ public class StringAndListParam extends BaseAndListParam<StringOrListParam> {
return new StringOrListParam();
}
@CoverageIgnore
@Override
public StringAndListParam addAnd(StringOrListParam theValue) {
addValue(theValue);
return this;
}
public StringAndListParam addAnd(StringParam theValue) {
addValue(new StringOrListParam().addOr(theValue));
return this;
}
}

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.util.CoverageIgnore;
import org.apache.commons.lang3.Validate;
/*
* #%L
@ -30,11 +30,23 @@ public class TokenAndListParam extends BaseAndListParam<TokenOrListParam> {
return new TokenOrListParam();
}
@CoverageIgnore
@Override
public TokenAndListParam addAnd(TokenOrListParam theValue) {
addValue(theValue);
return this;
}
/**
* @param theValue The OR values
* @return Returns a reference to this for convenient chaining
*/
public TokenAndListParam addAnd(TokenParam... theValue) {
Validate.notNull(theValue, "theValue must not be null");
TokenOrListParam orListParam = new TokenOrListParam();
for (TokenParam next : theValue) {
orListParam.add(next);
}
addValue(orListParam);
return this;
}
}

View File

@ -77,6 +77,14 @@ public class TokenOrListParam extends BaseOrListParam<TokenOrListParam, TokenPar
return this;
}
/**
* Add a new token to this list
*/
public TokenOrListParam add(String theValue) {
add(new TokenParam(null, theValue));
return this;
}
public List<BaseCodingDt> getListAsCodings() {
ArrayList<BaseCodingDt> retVal = new ArrayList<BaseCodingDt>();
for (TokenParam next : getValuesAsQueryTokens()) {

View File

@ -16,9 +16,9 @@ import java.util.*;
* 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.

View File

@ -0,0 +1,55 @@
package ca.uhn.fhir.util;
/*-
* #%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 org.slf4j.Logger;
import org.slf4j.event.Level;
/**
* Utility to fill a glaring gap in SLF4j's API - The fact that you can't
* specify a log level at runtime.
*
* See here for a discussion:
* https://jira.qos.ch/browse/SLF4J-124
*/
public class LogUtil {
public static void log(Logger theLogger, Level theLevel, String theMessage, Object... theArgs) {
switch (theLevel) {
case TRACE:
theLogger.trace(theMessage, theArgs);
break;
case DEBUG:
theLogger.debug(theMessage, theArgs);
break;
case INFO:
theLogger.info(theMessage, theArgs);
break;
case WARN:
theLogger.warn(theMessage, theArgs);
break;
case ERROR:
theLogger.error(theMessage, theArgs);
break;
}
}
}

View File

@ -20,29 +20,204 @@ package ca.uhn.fhir.util;
* #L%
*/
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Provides server ports
* Provides server ports that are free, in order for tests to use them
*
* <p><b>
* This class is ONLY designed for unit-testing usage, as it holds on to server ports
* for a long time (potentially lots of them!) and will leave your system low on
* ports if you put it into production.
* </b></p>
* <p>
* How it works:
* <p>
* We have lots of tests that need a free port because they want to open up
* a server, and need the port to be unique and unused so that the tests can
* run multithreaded. This turns out to just be an awful problem to solve for
* lots of reasons:
* <p>
* 1. You can request a free port from the OS by calling <code>new ServerSocket(0);</code>
* and this seems to work 99% of the time, but occasionally on a heavily loaded
* server if two processes ask at the exact same time they will receive the
* same port assignment, and one will fail.
* 2. Tests run in separate processes, so we can't just rely on keeping a collection
* of assigned ports or anything like that.
* <p>
* So we solve this like this:
* <p>
* At random, this class will pick a "control port" and bind it. A control port
* is just a randomly chosen port that is a multiple of 100. If we can bind
* successfully to that port, we now own the range of "n+1 to n+99". If we can't
* bind that port, it means some other process has probably taken it so
* we'll just try again until we find an available control port.
* <p>
* Assuming we successfully bind a control port, we'll give out any available
* ports in the range "n+1 to n+99" until we've exhausted the whole set, and
* then we'll pick another control port (if we actually get asked for over
* 100 ports.. this should be a rare event).
* <p>
* This mechanism has the benefit of (fingers crossed) being bulletproof
* in terms of its ability to give out ports that are actually free, thereby
* preventing random test failures.
* <p>
* This mechanism has the drawback of never giving up a control port once
* it has assigned one. To be clear, this class is deliberately leaking
* resources. Again, no production use!
*/
@CoverageIgnore
public class PortUtil {
private static final int SPACE_SIZE = 100;
private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
/*
* Non instantiable
private static final PortUtil INSTANCE = new PortUtil();
private static int ourPortDelay = 500;
private List<ServerSocket> myControlSockets = new ArrayList<>();
private Integer myCurrentControlSocketPort = null;
private int myCurrentOffset = 0;
/**
* Constructor -
*/
private PortUtil() {
PortUtil() {
// nothing
}
/**
* Clear and release all control sockets
*/
synchronized void clearInstance() {
for (ServerSocket next : myControlSockets) {
ourLog.info("Releasing control port: {}", next.getLocalPort());
try {
next.close();
} catch (IOException theE) {
// ignore
}
}
myControlSockets.clear();
myCurrentControlSocketPort = null;
}
/**
* Clear and release all control sockets
*/
synchronized int getNextFreePort() {
while (true) {
// Acquire a control port
while (myCurrentControlSocketPort == null) {
int nextCandidate = (int) (Math.random() * 65000.0);
nextCandidate = nextCandidate - (nextCandidate % SPACE_SIZE);
if (nextCandidate < 10000) {
continue;
}
try {
ServerSocket server = new ServerSocket();
server.setReuseAddress(true);
server.bind(new InetSocketAddress("localhost", nextCandidate));
myControlSockets.add(server);
ourLog.info("Acquired control socket on port {}", nextCandidate);
myCurrentControlSocketPort = nextCandidate;
myCurrentOffset = 0;
} catch (IOException theE) {
ourLog.info("Candidate control socket {} is already taken", nextCandidate);
continue;
}
}
// Find a free port within the allowable range
while (true) {
myCurrentOffset++;
if (myCurrentOffset == SPACE_SIZE) {
// Current space is exhausted
myCurrentControlSocketPort = null;
break;
}
int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset;
// Try to open a port on this socket and use it
if (!isAvailable(nextCandidatePort)) {
continue;
}
// Log who asked for the port, just in case that's useful
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previousElement = Arrays.stream(stackTraceElements)
.filter(t -> !t.toString().contains("PortUtil.") && !t.toString().contains("getStackTrace"))
.findFirst()
.orElse(stackTraceElements[2]);
ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString());
try {
Thread.sleep(ourPortDelay);
} catch (InterruptedException theE) {
// ignore
}
return nextCandidatePort;
}
}
}
@VisibleForTesting
public static void setPortDelay(Integer thePortDelay) {
if (thePortDelay == null) {
thePortDelay = 500;
} else {
ourPortDelay = thePortDelay;
}
}
/**
* This method checks if we are able to bind a given port to both
* 0.0.0.0 and localhost in order to be sure it's truly available.
*/
private static boolean isAvailable(int thePort) {
ourLog.info("Testing a bind on thePort {}", thePort);
try (ServerSocket ss = new ServerSocket()) {
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress("0.0.0.0", thePort));
try (DatagramSocket ds = new DatagramSocket()) {
ds.setReuseAddress(true);
ds.connect(new InetSocketAddress("127.0.0.1", thePort));
ourLog.info("Successfully bound thePort {}", thePort);
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
try (ServerSocket ss = new ServerSocket()) {
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress("localhost", thePort));
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
return true;
}
/**
* The entire purpose here is to find an available port that can then be
* bound for by server in a unit test without conflicting with other tests.
@ -51,64 +226,7 @@ public class PortUtil {
* so it can be reused across modules. Use with caution.
*/
public static int findFreePort() {
ServerSocket server;
try {
server = new ServerSocket(0);
server.setReuseAddress(true);
int port = server.getLocalPort();
/*
* Try to connect to the newly allocated port to make sure
* it's free
*/
for (int i = 0; i < 10; i++) {
try {
Socket client = new Socket();
client.connect(new InetSocketAddress(port), 1000);
break;
} catch (Exception e) {
if (i == 9) {
throw new InternalErrorException("Can not connect to port: " + port);
}
Thread.sleep(250);
}
}
server.close();
/*
* This is an attempt to make sure the port is actually
* free before releasing it. For whatever reason on Linux
* it seems like even after we close the ServerSocket there
* is a short while where it is not possible to bind the
* port, even though it should be released by then.
*
* I don't have any solid evidence that this is a good
* way to do this, but it seems to help...
*/
for (int i = 0; i < 10; i++) {
try {
Socket client = new Socket();
client.connect(new InetSocketAddress(port), 1000);
ourLog.info("Socket still seems open");
Thread.sleep(250);
} catch (Exception e) {
break;
}
}
// ....annnd sleep a bit for the same reason.
Thread.sleep(500);
// Log who asked for the port, just in case that's useful
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previousElement = stackTraceElements[2];
ourLog.info("Returned available port {} for: {}", port, previousElement.toString());
return port;
} catch (IOException | InterruptedException e) {
throw new Error(e);
}
return INSTANCE.getNextFreePort();
}
}

View File

@ -7,7 +7,6 @@ import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -45,20 +44,16 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class StopWatch {
private static final NumberFormat DAY_FORMAT = new DecimalFormat("0.0");
private static final NumberFormat TEN_DAY_FORMAT = new DecimalFormat("0");
private static Long ourNowForUnitTest;
private long myStarted = now();
private TaskTiming myCurrentTask;
private LinkedList<TaskTiming> myTasks;
/**
* Constructor
*/
public StopWatch() {
super();
}
/**
* Constructor
*
@ -93,6 +88,19 @@ public class StopWatch {
}
}
/**
* Returns a nice human-readable display of the time taken per
* operation. Note that this may not actually output the number
* of milliseconds if the time taken per operation was very long (over
* 10 seconds)
*
* @see #formatMillis(long)
*/
public String formatMillisPerOperation(int theNumOperations) {
double millisPerOperation = (((double) getMillis()) / Math.max(1.0, theNumOperations));
return formatMillis(millisPerOperation);
}
/**
* Returns a string providing the durations of all tasks collected by {@link #startTask(String)}
*/
@ -261,77 +269,6 @@ public class StopWatch {
return formatMillis(getMillis());
}
/**
* Append a right-aligned and zero-padded numeric value to a `StringBuilder`.
*/
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
tgt.append(pfx);
if (dgt > 1) {
int pad = (dgt - 1);
for (long xa = val; xa > 9 && pad > 0; xa /= 10) {
pad--;
}
for (int xa = 0; xa < pad; xa++) {
tgt.append('0');
}
}
tgt.append(val);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(long val) {
StringBuilder buf = new StringBuilder(20);
if (val < (10 * DateUtils.MILLIS_PER_SECOND)) {
buf.append(val);
buf.append("ms");
} else if (val >= DateUtils.MILLIS_PER_DAY) {
double days = (double) val / DateUtils.MILLIS_PER_DAY;
if (days >= 10) {
buf.append(TEN_DAY_FORMAT.format(days));
buf.append(" days");
} else if (days != 1.0f) {
buf.append(DAY_FORMAT.format(days));
buf.append(" days");
} else {
buf.append(DAY_FORMAT.format(days));
buf.append(" day");
}
} else {
append(buf, "", 2, ((val % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR));
append(buf, ":", 2, ((val % DateUtils.MILLIS_PER_HOUR) / DateUtils.MILLIS_PER_MINUTE));
append(buf, ":", 2, ((val % DateUtils.MILLIS_PER_MINUTE) / DateUtils.MILLIS_PER_SECOND));
if (val <= DateUtils.MILLIS_PER_MINUTE) {
append(buf, ".", 3, (val % DateUtils.MILLIS_PER_SECOND));
}
}
return buf.toString();
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTestForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
private static class TaskTiming {
private long myStart;
private long myEnd;
@ -372,4 +309,109 @@ public class StopWatch {
}
}
private static NumberFormat getDayFormat() {
return new DecimalFormat("0.0");
}
private static NumberFormat getTenDayFormat() {
return new DecimalFormat("0");
}
private static NumberFormat getSubMillisecondMillisFormat() {
return new DecimalFormat("0.000");
}
/**
* Append a right-aligned and zero-padded numeric value to a `StringBuilder`.
*/
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
tgt.append(pfx);
if (dgt > 1) {
int pad = (dgt - 1);
for (long xa = val; xa > 9 && pad > 0; xa /= 10) {
pad--;
}
for (int xa = 0; xa < pad; xa++) {
tgt.append('0');
}
}
tgt.append(val);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(long theMillis) {
return formatMillis((double) theMillis);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(double theMillis) {
StringBuilder buf = new StringBuilder(20);
if (theMillis > 0.0 && theMillis < 1.0) {
buf.append(getSubMillisecondMillisFormat().format(theMillis));
buf.append("ms");
} else if (theMillis < (10 * DateUtils.MILLIS_PER_SECOND)) {
buf.append((int) theMillis);
buf.append("ms");
} else if (theMillis >= DateUtils.MILLIS_PER_DAY) {
double days = theMillis / DateUtils.MILLIS_PER_DAY;
if (days >= 10) {
buf.append(getTenDayFormat().format(days));
buf.append(" days");
} else if (days != 1.0f) {
buf.append(getDayFormat().format(days));
buf.append(" days");
} else {
buf.append(getDayFormat().format(days));
buf.append(" day");
}
} else {
long millisAsLong = (long) theMillis;
append(buf, "", 2, ((millisAsLong % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR));
append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_HOUR) / DateUtils.MILLIS_PER_MINUTE));
append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_MINUTE) / DateUtils.MILLIS_PER_SECOND));
if (theMillis <= DateUtils.MILLIS_PER_MINUTE) {
append(buf, ".", 3, (millisAsLong % DateUtils.MILLIS_PER_SECOND));
}
}
return buf.toString();
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTestForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
}

View File

@ -63,6 +63,7 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.forcedIdConstraintFailure=The operation has failed with a client-assigned ID constraint failure. This typically means that multiple client threads are trying to create a new resource with the same client-assigned ID at the same time, and this thread was chosen to be rejected.
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}"
@ -88,6 +89,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedIdNo
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidParameterChain=Invalid parameter chain: {0}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidVersion=Version "{0}" is not valid for resource {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.multipleParamsWithSameNameOneIsMissingTrue=This server does not know how to handle multiple "{0}" parameters where one has a value of :missing=true
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.missingBody=No body was supplied in request
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulCreate=Successfully created resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.i18n;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.Set;
@ -10,6 +11,15 @@ import org.junit.Test;
public class HapiLocalizerTest {
@Test
public void testEscapePatterns() {
HapiLocalizer loc = new HapiLocalizer();
assertEquals("some message", loc.newMessageFormat("some message").format(new Object[]{}));
assertEquals("var1 {var2} var3 {var4}", loc.newMessageFormat("var1 {var2} var3 {var4}").format(new Object[]{}));
assertEquals("var1 A var3 B", loc.newMessageFormat("var1 {0} var3 {1}").format(new Object[]{ "A", "B"}));
}
@Test
public void testAllKeys() {

View File

@ -0,0 +1,588 @@
package ca.uhn.fhir.interceptor.executor;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.StopWatch;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
public class InterceptorServiceTest {
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorServiceTest.class);
private List<String> myInvocations = new ArrayList<>();
@Test
public void testInterceptorWithAnnotationDefinedOnInterface() {
InterceptorService svc = new InterceptorService();
TestInterceptorWithAnnotationDefinedOnInterface_Class interceptor = new TestInterceptorWithAnnotationDefinedOnInterface_Class();
svc.registerInterceptor(interceptor);
assertEquals(1, interceptor.getRegisterCount());
}
@Test
public void testInterceptorThrowsException() {
class InterceptorThrowingException {
@Hook(Pointcut.TEST_RB)
public void test(String theValue) {
throw new AuthenticationException(theValue);
}
}
InterceptorService svc = new InterceptorService();
svc.registerInterceptor(new InterceptorThrowingException());
try {
svc.callHooks(Pointcut.TEST_RB, new HookParams("A MESSAGE", "B"));
fail();
} catch (AuthenticationException e) {
assertEquals("A MESSAGE", e.getMessage());
}
}
@Test
public void testInterceptorReturnsClass() {
class InterceptorReturningClass {
private BaseServerResponseException myNextResponse;
@Hook(Pointcut.TEST_RO)
public BaseServerResponseException hook() {
return myNextResponse;
}
}
InterceptorReturningClass interceptor0 = new InterceptorReturningClass();
InterceptorReturningClass interceptor1 = new InterceptorReturningClass();
InterceptorService svc = new InterceptorService();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
interceptor0.myNextResponse = new InvalidRequestException("0");
interceptor1.myNextResponse = new InvalidRequestException("1");
Object response = svc.callHooksAndReturnObject(Pointcut.TEST_RO, new HookParams("", ""));
assertEquals("0", ((InvalidRequestException) response).getMessage());
interceptor0.myNextResponse = null;
response = svc.callHooksAndReturnObject(Pointcut.TEST_RO, new HookParams("", ""));
assertEquals("1", ((InvalidRequestException) response).getMessage());
}
@Test
public void testRegisterHookFails() {
InterceptorService svc = new InterceptorService();
int initialSize = svc.getGlobalInterceptorsForUnitTest().size();
try {
svc.registerInterceptor(new InterceptorThatFailsOnRegister());
fail();
} catch (InternalErrorException e) {
// good
}
assertEquals(initialSize, svc.getGlobalInterceptorsForUnitTest().size());
}
@Test
public void testManuallyRegisterInterceptor() {
InterceptorService svc = new InterceptorService();
// Registered in opposite order to verify that the order on the annotation is used
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
svc.registerInterceptor(interceptor1);
svc.registerInterceptor(interceptor0);
// Register the manual interceptor (has Order right in the middle)
MyTestInterceptorManual myInterceptorManual = new MyTestInterceptorManual();
svc.registerInterceptor(myInterceptorManual);
List<Object> globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Try to register again (should have no effect
svc.registerInterceptor(myInterceptorManual);
globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Make sure we have the right invokers in the right order
List<Object> invokers = svc.getInterceptorsWithInvokersForPointcut(Pointcut.TEST_RB);
assertSame(interceptor0, invokers.get(0));
assertSame(myInterceptorManual, invokers.get(1));
assertSame(interceptor1, invokers.get(2));
// Finally, unregister it
svc.unregisterInterceptor(myInterceptorManual);
globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(2, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo);
}
@Test
public void testInvokeGlobalInterceptorMethods() {
InterceptorService svc = new InterceptorService();
// Registered in opposite order to verify that the order on the annotation is used
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
svc.registerInterceptor(interceptor1);
svc.registerInterceptor(interceptor0);
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertTrue(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb", "MyTestInterceptorTwo.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame("A", interceptor1.myLastString0);
assertSame("B", interceptor1.myLastString1);
}
@Test
public void testInvokeUsingSupplierArg() {
InterceptorService svc = new InterceptorService();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertTrue(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb", "MyTestInterceptorTwo.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame("A", interceptor1.myLastString0);
assertSame("B", interceptor1.myLastString1);
}
@Test
public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() {
InterceptorService svc = new InterceptorService();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
interceptor0.myNextReturn = false;
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertFalse(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame(null, interceptor1.myLastString0);
assertSame(null, interceptor1.myLastString1);
}
@Test
public void testCallHooksInvokedWithNullParameters() {
InterceptorService svc = new InterceptorService();
class NullParameterInterceptor {
private String myValue0 = "";
private String myValue1 = "";
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myValue0 = theValue0;
myValue1 = theValue1;
}
}
NullParameterInterceptor interceptor;
HookParams params;
// Both null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, null)
.add(String.class, null);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(null, interceptor.myValue0);
assertEquals(null, interceptor.myValue1);
svc.unregisterAllInterceptors();
// First null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, null)
.add(String.class, "A");
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(null, interceptor.myValue0);
assertEquals("A", interceptor.myValue1);
svc.unregisterAllInterceptors();
// Second null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, "A")
.add(String.class, null);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals("A", interceptor.myValue0);
assertEquals(null, interceptor.myValue1);
svc.unregisterAllInterceptors();
}
@Test
public void testCallHooksLogAndSwallowException() {
InterceptorService svc = new InterceptorService();
class LogAndSwallowInterceptor0 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new IllegalStateException();
}
}
LogAndSwallowInterceptor0 interceptor0 = new LogAndSwallowInterceptor0();
svc.registerInterceptor(interceptor0);
class LogAndSwallowInterceptor1 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new IllegalStateException();
}
}
LogAndSwallowInterceptor1 interceptor1 = new LogAndSwallowInterceptor1();
svc.registerInterceptor(interceptor1);
class LogAndSwallowInterceptor2 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new NullPointerException("AAA");
}
}
LogAndSwallowInterceptor2 interceptor2 = new LogAndSwallowInterceptor2();
svc.registerInterceptor(interceptor2);
HookParams params = new HookParams()
.add(String.class, null)
.add(String.class, null);
try {
svc.callHooks(Pointcut.TEST_RB, params);
fail();
} catch (NullPointerException e) {
assertEquals("AAA", e.getMessage());
}
assertEquals(true, interceptor0.myHit);
assertEquals(true, interceptor1.myHit);
assertEquals(true, interceptor2.myHit);
}
@Test
public void testCallHooksInvokedWithWrongParameters() {
InterceptorService svc = new InterceptorService();
Integer msg = 123;
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
try {
svc.callHooks(Pointcut.TEST_RB, params);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("Invalid params for pointcut " + Pointcut.TEST_RB + " - Wanted java.lang.String,java.lang.String but found "));
}
}
@Test
public void testValidateParamTypes() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
params.add(String.class, "B");
boolean validated = svc.haveAppropriateParams(Pointcut.TEST_RB, params);
assertTrue(validated);
}
@Test
public void testValidateParamTypesMissingParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
try {
svc.haveAppropriateParams(Pointcut.TEST_RB, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut " + Pointcut.TEST_RB + " - Wanted java.lang.String,java.lang.String but found [String]", e.getMessage());
}
}
@Test
public void testValidateParamTypesExtraParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
params.add(String.class, "B");
params.add(String.class, "C");
params.add(String.class, "D");
params.add(String.class, "E");
try {
svc.haveAppropriateParams(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut " + Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED + " - Wanted ca.uhn.fhir.rest.api.server.RequestDetails,ca.uhn.fhir.rest.server.servlet.ServletRequestDetails,org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [String, String, String, String, String]", e.getMessage());
}
}
@SuppressWarnings("unchecked")
@Test
public void testValidateParamTypesWrongParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add((Class) String.class, 1);
params.add((Class) String.class, 2);
params.add((Class) String.class, 3);
params.add((Class) String.class, 4);
try {
svc.haveAppropriateParams(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid params for pointcut " + Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED + " - class java.lang.Integer is not of type class java.lang.String", e.getMessage());
}
}
@Test
public void testThreadLocalHookInterceptor() {
InterceptorService svc = new InterceptorService();
svc.setThreadlocalInvokersEnabled(true);
HookParams params = new HookParams().add("A").add("B");
@Interceptor(order = 100)
class LocalInterceptor {
private int myCount = 0;
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0, String theString1) {
myCount++;
return true;
}
}
LocalInterceptor interceptor = new LocalInterceptor();
svc.registerThreadLocalInterceptor(interceptor);
try {
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(5, interceptor.myCount);
} finally {
svc.unregisterThreadLocalInterceptor(interceptor);
}
// Call some more - The interceptor is removed so the count shouldn't change
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(5, interceptor.myCount);
}
/**
* <pre>
* JA 20190321 On my MBP 2018
* ThreadLocalEnabled=true - Performed 500000 loops in 8383.0ms - 0.017ms / loop
* ThreadLocalEnabled=false - Performed 500000 loops in 3743.0ms - 0.007ms / loop
* ThreadLocalEnabled=true - Performed 500000 loops in 6163.0ms - 0.012ms / loop
* ThreadLocalEnabled=false - Performed 500000 loops in 3487.0ms - 0.007ms / loop
* ThreadLocalEnabled=true - Performed 1000000 loops in 00:00:12.458 - 0.012ms / loop
* ThreadLocalEnabled=false - Performed 1000000 loops in 7046.0ms - 0.007ms / loop
* </pre>
*/
@Test
@Ignore("Performance test - Not needed normally")
public void testThreadLocalHookInterceptorMicroBenchmark() {
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
}
private void threadLocalMicroBenchmark(boolean theThreadlocalInvokersEnabled, int theCount) {
InterceptorService svc = new InterceptorService();
svc.setThreadlocalInvokersEnabled(theThreadlocalInvokersEnabled);
HookParams params = new HookParams().add("A").add("B");
@Interceptor(order = 100)
class LocalInterceptor {
private int myCount = 0;
@Hook(Pointcut.TEST_RB)
public void testRb(String theString0, String theString1) {
myCount++;
}
}
LocalInterceptor interceptor = new LocalInterceptor();
StopWatch sw = new StopWatch();
for (int i = 0; i < theCount; i++) {
svc.registerThreadLocalInterceptor(interceptor);
try {
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
} finally {
svc.unregisterThreadLocalInterceptor(interceptor);
}
}
ourLog.info("ThreadLocalEnabled={} - Performed {} loops in {} - {} / loop - Outcomne: {}", theThreadlocalInvokersEnabled, theCount, sw.toString(), sw.formatMillisPerOperation(theCount), interceptor.myCount);
}
@Before
public void before() {
myInvocations.clear();
}
interface TestInterceptorWithAnnotationDefinedOnInterface_Interface {
@Hook(Pointcut.INTERCEPTOR_REGISTERED)
void registered();
}
@Interceptor(order = 100)
public class MyTestInterceptorOne {
private String myLastString0;
private boolean myNextReturn = true;
public MyTestInterceptorOne() {
super();
}
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0) {
myLastString0 = theString0;
myInvocations.add("MyTestInterceptorOne.testRb");
return myNextReturn;
}
}
@Interceptor(order = 300)
public class MyTestInterceptorTwo {
private String myLastString0;
private String myLastString1;
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0, String theString1) {
myLastString0 = theString0;
myLastString1 = theString1;
myInvocations.add("MyTestInterceptorTwo.testRb");
return true;
}
}
@Interceptor(order = 200)
public class MyTestInterceptorManual {
@Hook(Pointcut.TEST_RB)
public void testRb() {
myInvocations.add("MyTestInterceptorManual.testRb");
}
}
public static class TestInterceptorWithAnnotationDefinedOnInterface_Class implements TestInterceptorWithAnnotationDefinedOnInterface_Interface {
private int myRegisterCount = 0;
public int getRegisterCount() {
return myRegisterCount;
}
@Override
public void registered() {
myRegisterCount++;
}
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class CanonicalSubscription {
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class ResourceDeliveryMessage {
}
@Interceptor()
public static class InterceptorThatFailsOnRegister {
@Hook(Pointcut.INTERCEPTOR_REGISTERED)
public void start() throws Exception {
throw new Exception("InterceptorThatFailsOnRegister FAILED!");
}
}
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.util;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
public class LogUtilTest {
@Test
public void testLevels() {
Logger log = mock(Logger.class);
LogUtil.log(log, Level.TRACE, "HELLO");
LogUtil.log(log, Level.DEBUG, "HELLO");
LogUtil.log(log, Level.INFO, "HELLO");
LogUtil.log(log, Level.WARN, "HELLO");
LogUtil.log(log, Level.ERROR, "HELLO");
verify(log, times(1)).trace(anyString(),any(Object[].class));
verify(log, times(1)).debug(anyString(),any(Object[].class));
verify(log, times(1)).info(anyString(),any(Object[].class));
verify(log, times(1)).warn(anyString(),any(Object[].class));
verify(log, times(1)).error(anyString(),any(Object[].class));
verifyNoMoreInteractions(log);
}
}

View File

@ -1,15 +1,27 @@
package ca.uhn.fhir.util;
import org.junit.After;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.*;
public class PortUtilTest {
private static final Logger ourLog = LoggerFactory.getLogger(PortUtilTest.class);
@Test
public void testFindFreePort() throws IOException {
int port = PortUtil.findFreePort();
@ -28,4 +40,68 @@ public class PortUtilTest {
}
}
@After
public void after() {
PortUtil.setPortDelay(null);
}
@Test
public void testPortsAreNotReused() throws InterruptedException {
PortUtil.setPortDelay(0);
List<Integer> ports = Collections.synchronizedList(new ArrayList<>());
List<PortUtil> portUtils = Collections.synchronizedList(new ArrayList<>());
List<String> errors = Collections.synchronizedList(new ArrayList<>());
int tasksCount = 20;
ExecutorService pool = Executors.newFixedThreadPool(tasksCount);
int portsPerTaskCount = 151;
for (int i = 0; i < tasksCount; i++) {
pool.submit(() -> {
PortUtil portUtil = new PortUtil();
portUtils.add(portUtil);
for (int j = 0; j < portsPerTaskCount; j++) {
int nextFreePort = portUtil.getNextFreePort();
boolean bound;
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress("localhost", nextFreePort));
bound = true;
} catch (IOException e) {
bound = false;
}
if (!bound) {
try (ServerSocket ss = new ServerSocket()) {
Thread.sleep(1000);
ss.bind(new InetSocketAddress("localhost", nextFreePort));
} catch (Exception e) {
String msg = "Failure binding new port (second attempt) " + nextFreePort + ": " + e.toString();
ourLog.error(msg, e);
errors.add(msg);
}
}
ports.add(nextFreePort);
}
});
}
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
assertThat(errors.toString(), errors, empty());
assertEquals(tasksCount * portsPerTaskCount, ports.size());
while (ports.size() > 0) {
Integer nextPort = ports.remove(0);
if (ports.contains(nextPort)) {
fail("Port " + nextPort + " was given out more than once");
}
}
for (PortUtil next : portUtils) {
next.clearInstance();
}
}
}

View File

@ -9,7 +9,8 @@ import java.util.Date;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class StopWatchTest {
@ -99,6 +100,7 @@ public class StopWatchTest {
@Test
public void testFormatMillis() {
assertEquals("0.134ms", StopWatch.formatMillis(0.1339d).replace(',', '.'));
assertEquals("1000ms", StopWatch.formatMillis(DateUtils.MILLIS_PER_SECOND));
assertEquals("00:01:00.000", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE));
assertEquals("00:01:01", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE + DateUtils.MILLIS_PER_SECOND));

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.StructureDefinition;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -56,6 +57,11 @@ public class LoadingValidationSupportDstu3 implements IValidationSupport {
return null;
}
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return null;
}
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
String resName = myCtx.getResourceDefinition(theClass).getName();

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.r4.terminologies.ValueSetExpander;
@ -58,6 +59,11 @@ public class LoadingValidationSupportR4 implements org.hl7.fhir.r4.hapi.ctx.IVal
return null;
}
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return null;
}
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
String resName = myCtx.getResourceDefinition(theClass).getName();

View File

@ -7,7 +7,6 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -59,8 +58,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(
@ -72,9 +72,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal;
}

View File

@ -8,12 +8,9 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowire;
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.context.annotation.Import;
@ -73,8 +70,9 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(
@ -86,9 +84,10 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal;
}

View File

@ -64,8 +64,9 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/
public IServerInterceptor loggingInterceptor() {
public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(
@ -77,9 +78,10 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
/**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() {
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal;
}

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -66,8 +67,8 @@ public class JpaServerDemo extends RestfulServer {
throw new IllegalStateException();
}
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class);
setResourceProviders(beans);
ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
registerProviders(beans.createProviders());
/*
* The system provider implements non-resource-type methods, such as
@ -85,7 +86,7 @@ public class JpaServerDemo extends RestfulServer {
} else {
throw new IllegalStateException();
}
setPlainProviders(systemProvider);
registerProvider(systemProvider);
/*
* The conformance provider exports the supported resources, search parameters, etc for
@ -125,7 +126,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/*
* Default to XML and pretty printing

View File

@ -21,6 +21,10 @@ package ca.uhn.fhir.rest.client.impl;
*/
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.interceptor.api.HookParams;
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.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.*;
@ -43,7 +47,11 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import java.io.*;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -65,13 +73,13 @@ public abstract class BaseClient implements IRestfulClient {
private final String myUrlBase;
private boolean myDontValidateConformance;
private EncodingEnum myEncoding = null; // default unspecified (will be XML)
private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>();
private boolean myKeepResponses = false;
private IHttpResponse myLastResponse;
private String myLastResponseBody;
private Boolean myPrettyPrint = false;
private SummaryEnum mySummary;
private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT;
private IInterceptorService myInterceptorService;
BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) {
super();
@ -92,6 +100,18 @@ public abstract class BaseClient implements IRestfulClient {
myEncoding = EncodingEnum.JSON;
}
setInterceptorService(new InterceptorService());
}
@Override
public IInterceptorService getInterceptorService() {
return myInterceptorService;
}
@Override
public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) {
Validate.notNull(theInterceptorService, "theInterceptorService must not be null");
myInterceptorService = theInterceptorService;
}
protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) {
@ -149,14 +169,6 @@ public abstract class BaseClient implements IRestfulClient {
return myClient;
}
/**
* {@inheritDoc}
*/
@Override
public List<IClientInterceptor> getInterceptors() {
return Collections.unmodifiableList(myInterceptors);
}
/**
* For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
*/
@ -276,15 +288,16 @@ public abstract class BaseClient implements IRestfulClient {
}
}
for (IClientInterceptor nextInterceptor : myInterceptors) {
nextInterceptor.interceptRequest(httpRequest);
}
HookParams requestParams = new HookParams();
requestParams.add(IHttpRequest.class, httpRequest);
getInterceptorService().callHooks(Pointcut.CLIENT_REQUEST, requestParams);
response = httpRequest.execute();
for (IClientInterceptor nextInterceptor : myInterceptors) {
nextInterceptor.interceptResponse(response);
}
HookParams responseParams = new HookParams();
responseParams.add(IHttpRequest.class, httpRequest);
responseParams.add(IHttpResponse.class, response);
getInterceptorService().callHooks(Pointcut.CLIENT_RESPONSE, responseParams);
String mimeType;
if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) {
@ -446,7 +459,7 @@ public abstract class BaseClient implements IRestfulClient {
@Override
public void registerInterceptor(IClientInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.add(theInterceptor);
getInterceptorService().registerInterceptor(theInterceptor);
}
/**
@ -461,7 +474,7 @@ public abstract class BaseClient implements IRestfulClient {
@Override
public void unregisterInterceptor(IClientInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
getInterceptorService().unregisterInterceptor(theInterceptor);
}
protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> {

View File

@ -272,10 +272,9 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
@Override
public void validateServerBase(String theServerBase, IHttpClient theHttpClient, IRestfulClient theClient) {
GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
client.setInterceptorService(theClient.getInterceptorService());
client.setEncoding(theClient.getEncoding());
for (IClientInterceptor interceptor : theClient.getInterceptors()) {
client.registerInterceptor(interceptor);
}
client.setDontValidateConformance(true);
IBaseResource conformance;

View File

@ -67,6 +67,11 @@ public class IgPackValidationSupportDstu3 implements IValidationSupport {
return fetchResource(theContext, CodeSystem.class, theSystem);
}
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return fetchResource(theContext, ValueSet.class, theSystem);
}
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
for (Map.Entry<IIdType, IBaseResource> next : myIgResources.entrySet()) {

View File

@ -156,10 +156,10 @@ public abstract class AbstractJaxRsBundleProvider extends AbstractJaxRsProvider
/**
* Default: an empty list of interceptors
*
* @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors()
* @see ca.uhn.fhir.rest.server.IRestfulServerDefaults#getInterceptors_()
*/
@Override
public List<IServerInterceptor> getInterceptors() {
public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList();
}

View File

@ -33,6 +33,7 @@ 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.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.LoggerFactory;

View File

@ -25,6 +25,7 @@ import java.util.Map.Entry;
import javax.ws.rs.core.*;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.FhirContext;
@ -74,6 +75,11 @@ public abstract class AbstractJaxRsProvider implements IRestfulServerDefaults {
CTX = ctx;
}
@Override
public IInterceptorService getInterceptorService() {
return null;
}
/**
* DEFAULT = AddProfileTagEnum.NEVER
*/
@ -145,10 +151,10 @@ public abstract class AbstractJaxRsProvider implements IRestfulServerDefaults {
* Default: an empty list of interceptors (Interceptors are not yet supported
* in the JAX-RS server). Please get in touch if you'd like to help!
*
* @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors()
* @see ca.uhn.fhir.rest.server.IRestfulServerDefaults#getInterceptors_()
*/
@Override
public List<IServerInterceptor> getInterceptors() {
public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList();
}

View File

@ -28,6 +28,7 @@ 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;
@ -102,7 +103,7 @@ implements IRestfulServer<JaxRsRequest>, IResourceProvider {
theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass);
}
/**
/**
* The base for request for a resource provider has the following form:</br>
* {@link AbstractJaxRsResourceProvider#getBaseForServer()
* getBaseForServer()} + "/" +

View File

@ -67,6 +67,7 @@ public class JaxRsRequest extends RequestDetails {
*/
public JaxRsRequest(AbstractJaxRsProvider server, String resourceString, RequestTypeEnum requestType,
RestOperationTypeEnum restOperation) {
super(server.getInterceptorService());
this.myHeaders = server.getHeaders();
this.myResourceString = resourceString;
this.setRestOperationType(restOperation);

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jaxrs.server.test;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.dstu3.model.Patient;
import ca.uhn.fhir.context.FhirContext;
@ -18,4 +19,5 @@ public class TestJaxRsDummyPatientProviderDstu3 extends AbstractJaxRsResourcePro
public Class<Patient> getResourceType() {
return Patient.class;
}
}

View File

@ -127,7 +127,7 @@ public class JaxRsPatientRestProvider extends AbstractJaxRsResourceProvider<Pati
/** THE DEFAULTS */
@Override
public List<IServerInterceptor> getInterceptors() {
public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList();
}

View File

@ -128,7 +128,7 @@ public class JaxRsPatientRestProviderDstu3 extends AbstractJaxRsResourceProvider
/** THE DEFAULTS */
@Override
public List<IServerInterceptor> getInterceptors() {
public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList();
}

View File

@ -126,23 +126,16 @@
<version>${project.version}</version>
</dependency>
<!--
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
</dependency>
-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
@ -154,6 +147,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.helger</groupId>
@ -189,14 +186,8 @@
<!-- Patch Dependencies -->
<dependency>
<groupId>net.riotopsys</groupId>
<artifactId>json_patch</artifactId>
<exclusions>
<exclusion>
<artifactId>com.google.code.gson</artifactId>
<groupId>gson</groupId>
</exclusion>
</exclusions>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
</dependency>
<dependency>
<groupId>com.github.dnault</groupId>
@ -460,6 +451,10 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@ -523,11 +518,6 @@
<artifactId>greenmail-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>

View File

@ -2,6 +2,8 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@ -14,15 +16,16 @@ import ca.uhn.fhir.jpa.subscription.module.cache.ISubscribableChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.cache.LinkedBlockingQueueSubscribableChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.jpa.util.JpaInterceptorService;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
@ -71,11 +74,17 @@ 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)
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
return new DatabaseBackedPagingProvider();
@ -158,13 +167,36 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new CompositeInMemoryDaoSubscriptionMatcher(daoSubscriptionMatcher(), inMemorySubscriptionMatcher());
}
@Bean
public HapiFhirHibernateJpaDialect hibernateJpaDialect() {
return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer());
}
@Bean
public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
@Bean
public IInterceptorService jpaInterceptorService() {
return new JpaInterceptorService();
}
/**
* Subclasses may override
*/
protected boolean isSupported(String theResourceType) {
return daoRegistry().getResourceDaoIfExists(theResourceType) != null;
}
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");
theFactory.setPersistenceProvider(new HibernatePersistenceProvider());
}
private static HibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) {
private static HapiFhirHibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) {
return new HapiFhirHibernateJpaDialect(theLocalizer);
}
}

View File

@ -21,20 +21,28 @@ package ca.uhn.fhir.jpa.config;
*/
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hibernate.HibernateException;
import org.hibernate.StaleStateException;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import javax.persistence.PersistenceException;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
private static final Logger ourLog = LoggerFactory.getLogger(HapiFhirHibernateJpaDialect.class);
private HapiLocalizer myLocalizer;
/**
@ -44,18 +52,56 @@ public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
myLocalizer = theLocalizer;
}
public RuntimeException translate(PersistenceException theException, String theMessageToPrepend) {
if (theException.getCause() instanceof HibernateException) {
return new PersistenceException(convertHibernateAccessException((HibernateException) theException.getCause(), theMessageToPrepend));
}
return theException;
}
@Override
protected DataAccessException convertHibernateAccessException(HibernateException theException) {
return convertHibernateAccessException(theException, null);
}
private DataAccessException convertHibernateAccessException(HibernateException theException, String theMessageToPrepend) {
String messageToPrepend = "";
if (isNotBlank(theMessageToPrepend)) {
messageToPrepend = theMessageToPrepend + " - ";
}
if (theException instanceof ConstraintViolationException) {
String constraintName = ((ConstraintViolationException) theException).getConstraintName();
switch (defaultString(constraintName)) {
case ResourceHistoryTable.IDX_RESVER_ID_VER:
throw new ResourceVersionConflictException(myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"));
throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"));
case ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING:
throw new ResourceVersionConflictException(myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure"));
throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure"));
case ForcedId.IDX_FORCEDID_TYPE_FID:
throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "forcedIdConstraintFailure"));
}
}
/*
* It would be nice if we could be more precise here, since technically any optimistic lock
* failure could result in a StaleStateException, but with the error message we're returning
* we're basically assuming it's an optimistic lock failure on HFJ_RESOURCE.
*
* That said, I think this is an OK trade-off. There is a high probability that if this happens
* it is a failure on HFJ_RESOURCE (there aren't many other tables in our schema that
* use @Version at all) and this error message is infinitely more comprehensible
* than the one we'd otherwise return.
*
* The actual StaleStateException is thrown in hibernate's Expectations
* class in a method called "checkBatched" currently. This can all be tested using the
* StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction()
*/
if (theException instanceof StaleStateException) {
String msg = messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure");
throw new ResourceVersionConflictException(msg);
}
return super.convertHibernateAccessException(theException);
}

View File

@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.config;
* #L%
*/
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionWebsocketHandler;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.subscription.module.subscriber.websocket.SubscriptionWebsocketHandler;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
@ -35,10 +37,12 @@ import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
@EnableWebSocket()
@Controller
public class WebsocketDispatcherConfig implements WebSocketConfigurer {
@Autowired
ModelConfig myModelConfig;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) {
theRegistry.addHandler(subscriptionWebSocketHandler(), "/websocket").setAllowedOrigins("*");
theRegistry.addHandler(subscriptionWebSocketHandler(), myModelConfig.getWebsocketContextPath()).setAllowedOrigins("*");
}
@Bean(autowire = Autowire.BY_TYPE)

View File

@ -1,15 +1,16 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
@ -130,8 +131,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
@Autowired
protected IdHelperService myIdHelperService;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
protected ISearchResultDao mySearchResultDao;
@ -192,6 +191,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
private SearchBuilderFactory mySearchBuilderFactory;
private ApplicationContext myApplicationContext;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
/**
* Returns the newly created forced ID. If the entity already had a forced ID, or if
@ -223,6 +224,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
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() + ".");
}
AtomicInteger remainingCount = new AtomicInteger(theExpungeOptions.getLimit());
if (theResourceName == null && theResourceId == null && theVersion == null) {
@ -274,12 +279,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
for (Long next : resourceIds) {
txTemplate.execute(t -> {
expungeHistoricalVersionsOfId(next, remainingCount);
if (remainingCount.get() <= 0) {
ourLog.debug("Expunge limit has been hit - Stopping operation");
return toExpungeOutcome(theExpungeOptions, remainingCount);
}
return null;
});
if (remainingCount.get() <= 0) {
ourLog.debug("Expunge limit has been hit - Stopping operation");
return toExpungeOutcome(theExpungeOptions, remainingCount);
}
}
/*
@ -290,6 +295,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
expungeCurrentVersionOfResource(next, remainingCount);
return null;
});
if (remainingCount.get() <= 0) {
ourLog.debug("Expunge limit has been hit - Stopping operation");
return toExpungeOutcome(theExpungeOptions, remainingCount);
}
}
}
@ -301,8 +310,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/
Pageable page = PageRequest.of(0, remainingCount.get());
Slice<Long> historicalIds = txTemplate.execute(t -> {
if (theResourceId != null && theVersion != null) {
return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion));
if (theResourceId != null) {
if (theVersion != null) {
return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion));
} else {
return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theResourceId);
}
} else {
if (theResourceName != null) {
return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName);
@ -611,10 +624,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return myDaoRegistry.getResourceDaoIfExists(theType);
}
protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
return myDaoRegistry.getDaoOrThrowException(theClass);
}
protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
return null;
@ -771,10 +780,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) {
theRequestDetails.notifyIncomingRequestPreHandled(theOperationType);
}
List<IServerInterceptor> interceptors = getConfig().getInterceptors();
for (IServerInterceptor next : interceptors) {
next.incomingRequestPreHandled(theOperationType, theRequestDetails);
}
}
/**
@ -1306,17 +1311,18 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
if (changed.isChanged()) {
theEntity.setUpdated(theUpdateTime);
if (theResource instanceof IResource) {
theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
} else {
theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
}
theEntity.setUpdated(theUpdateTime);
if (theResource instanceof IResource) {
theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
} else {
theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
newParams.setParamsOn(theEntity);
theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
populateFullTextFields(myContext, theResource, theEntity);
}
newParams.setParamsOn(theEntity);
theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
populateFullTextFields(myContext, theResource, theEntity);
} else {
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
@ -1425,6 +1431,11 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) {
// We'll update the resource ID with the correct version later but for
// now at least set it to something useful for the interceptors
theResource.setId(theEntity.getIdDt());
// Notify interceptors
ActionRequestDetails requestDetails;
if (theRequestDetails != null) {
@ -1433,18 +1444,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
// Notify IServerOperationInterceptors about pre-action call
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourcePreUpdate(theOldResource, theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
}
}
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams);
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams);
// Perform update
ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
@ -1466,23 +1471,15 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
// Notify interceptors
if (!savedEntity.isUnchangedInCurrentOperation()) {
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
theRequestDetails.getRequestOperationCallback().resourceUpdated(theOldResource, theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams);
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
}
});
}
@ -1550,15 +1547,17 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
continue;
}
for (IBase nextChild : values) {
IBaseReference nextRef = (IBaseReference) nextChild;
IIdType referencedId = nextRef.getReferenceElement();
if (!isBlank(referencedId.getResourceType())) {
if (!isLogicalReference(referencedId)) {
if (!referencedId.getValue().contains("?")) {
if (!validTypes.contains(referencedId.getResourceType())) {
throw new UnprocessableEntityException(
"Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
if (getConfig().isEnforceReferenceTargetTypes()) {
for (IBase nextChild : values) {
IBaseReference nextRef = (IBaseReference) nextChild;
IIdType referencedId = nextRef.getReferenceElement();
if (!isBlank(referencedId.getResourceType())) {
if (!isLogicalReference(referencedId)) {
if (!referencedId.getValue().contains("?")) {
if (!validTypes.contains(referencedId.getResourceType())) {
throw new UnprocessableEntityException(
"Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
}
}
}
}

View File

@ -24,10 +24,11 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
@ -51,6 +52,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
@ -70,6 +72,7 @@ import javax.annotation.PostConstruct;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -141,6 +144,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTimestamp, RequestDetails theRequestDetails) {
if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody");
throw new InvalidRequestException(msg);
}
if (isNotBlank(theResource.getIdElement().getIdPart())) {
if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
@ -212,14 +220,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, null, false);
// Notify IServerOperationInterceptors about pre-action call
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
}
}
HookParams hook = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook);
validateOkToDelete(theDeleteConflicts, entity, false);
@ -236,25 +241,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors
if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId);
theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams);
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true);
DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resourceToDelete).setCreated(true);
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
@ -302,14 +300,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, null, false);
// Notify IServerOperationInterceptors about pre-action call
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
}
}
HookParams hooks = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
validateOkToDelete(deleteConflicts, entity, false);
@ -326,21 +321,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams);
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
}
@ -404,7 +392,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity = myEntityManager.find(ResourceTable.class, pid);
IBaseResource resource = toResource(entity, false);
theResource.setId(resource.getIdElement().getValue());
return toMethodOutcome(entity, resource).setCreated(false);
return toMethodOutcome(theRequest, entity, resource).setCreated(false);
}
}
@ -437,17 +425,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
// Notify JPA interceptors
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourcePreCreate(theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource);
}
}
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, hookParams);
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
// Perform actual DB update
ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
@ -482,25 +464,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify JPA interceptors
if (!updatedEntity.isUnchangedInCurrentOperation()) {
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourceCreated(theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceCreated(theRequest, theResource);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
}
}
});
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, hookParams);
}
});
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
}
@ -919,7 +895,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
validateResourceTypeAndThrowInvalidRequestException(theId);
// Notify interceptors
if (theRequestDetails != null) {
@ -941,8 +917,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
HookParams params = new HookParams().add(IBaseResource.class, retVal);
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
HookParams params = new HookParams()
.add(IBaseResource.class, retVal)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCE, params);
ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal;
@ -956,7 +935,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
validateResourceTypeAndThrowInvalidRequestException(theId);
Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
@ -1002,6 +981,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theId);
}
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
entity.setTransientForcedId(theId.getIdPart());
return entity;
}
@ -1071,7 +1051,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
for (List<List<? extends IQueryParameterType>> nextAnds : theParams.values()) {
for (List<List<IQueryParameterType>> nextAnds : theParams.values()) {
for (List<? extends IQueryParameterType> nextOrs : nextAnds) {
for (IQueryParameterType next : nextOrs) {
if (next.getMissing() != null) {
@ -1131,9 +1111,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
HashSet<Long> retVal = new HashSet<Long>();
String uuid = UUID.randomUUID().toString();
Iterator<Long> iter = builder.createQuery(theParams, uuid);
while (iter.hasNext()) {
retVal.add(iter.next());
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(uuid);
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails)) {
while (iter.hasNext()) {
retVal.add(iter.next());
}
} catch (IOException e) {
ourLog.error("IO failure during database access", e);
}
return retVal;
@ -1174,7 +1159,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal;
}
private DaoMethodOutcome toMethodOutcome(@Nonnull final ResourceTable theEntity, @Nonnull IBaseResource theResource) {
private DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final ResourceTable theEntity, @Nonnull IBaseResource theResource) {
DaoMethodOutcome outcome = new DaoMethodOutcome();
IIdType id = null;
@ -1192,9 +1177,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
outcome.setResource(theResource);
outcome.setEntity(theEntity);
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
HookParams params = new HookParams().add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
// Interceptor broadcast
HookParams params = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCE, params);
return outcome;
}
@ -1266,6 +1254,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) {
if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody");
throw new InvalidRequestException(msg);
}
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
@ -1333,7 +1326,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
*/
if (!thePerformIndexing) {
theResource.setId(entity.getIdDt().getValue());
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(false);
DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, entity, theResource).setCreated(false);
outcome.setPreviousResource(oldResource);
return outcome;
}
@ -1342,7 +1335,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
* Otherwise, we're not in a transaction
*/
ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource);
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, theResource).setCreated(false);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
@ -1427,9 +1420,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
validateResourceType(entity, myResourceName);
}
private void validateResourceTypeAndThrowIllegalArgumentException(IIdType theId) {
private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) {
if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
throw new IllegalArgumentException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
// Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database exception
throw new InvalidRequestException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
}
}

View File

@ -114,7 +114,6 @@ public class DaoConfig {
* update setter javadoc if default changes
*/
private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors = new ArrayList<>();
/**
* update setter javadoc if default changes
*/
@ -142,6 +141,7 @@ public class DaoConfig {
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true;
private boolean myEnforceReferenceTargetTypes = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/**
@ -161,6 +161,26 @@ public class DaoConfig {
}
}
/**
* If set to <code>true</code> (default is true) when a resource is being persisted,
* the target resource types of references will be validated to ensure that they
* are appropriate for the field containing the reference. This is generally a good idea
* because invalid reference target types may not be searchable.
*/
public boolean isEnforceReferenceTargetTypes() {
return myEnforceReferenceTargetTypes;
}
/**
* If set to <code>true</code> (default is true) when a resource is being persisted,
* the target resource types of references will be validated to ensure that they
* are appropriate for the field containing the reference. This is generally a good idea
* because invalid reference target types may not be searchable.
*/
public void setEnforceReferenceTargetTypes(boolean theEnforceReferenceTargetTypes) {
myEnforceReferenceTargetTypes = theEnforceReferenceTargetTypes;
}
/**
* If a non-null value is supplied (default is <code>null</code>), a default
* for the <code>_total</code> parameter may be specified here. For example,
@ -506,47 +526,6 @@ public class DaoConfig {
myIndexMissingFieldsEnabled = theIndexMissingFields;
}
/**
* Returns the interceptors which will be notified of operations.
*
* @see #setInterceptors(List)
* @deprecated Marked as deprecated as of HAPI 3.7.0. Use {@link #registerInterceptor} or {@link #unregisterInterceptor}instead.
*/
@Deprecated
public List<IServerInterceptor> getInterceptors() {
return myInterceptors;
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(List<IServerInterceptor> theInterceptors) {
myInterceptors = theInterceptors;
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(IServerInterceptor... theInterceptor) {
setInterceptors(new ArrayList<>());
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
}
public void registerInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
if (!myInterceptors.contains(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
/**
* See {@link #setMaximumExpansionSize(int)}
*/
@ -1392,6 +1371,7 @@ public class DaoConfig {
/**
* If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions
* and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs.
*
* @since 3.7.0
*/
@ -1402,6 +1382,7 @@ public class DaoConfig {
/**
* If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions
* and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs.
*
* @since 3.7.0
*/
@ -1557,6 +1538,21 @@ public class DaoConfig {
myModelConfig.setEmailFromAddress(theEmailFromAddress);
}
/**
* If websocket subscriptions are enabled, this defines the context path that listens to them. Default value "/websocket".
*/
public String getWebsocketContextPath() {
return myModelConfig.getWebsocketContextPath();
}
/**
* If websocket subscriptions are enabled, this defines the context path that listens to them. Default value "/websocket".
*/
public void setWebsocketContextPath(String theWebsocketContextPath) {
myModelConfig.setWebsocketContextPath(theWebsocketContextPath);
}
public enum IndexEnabledEnum {
ENABLED,

View File

@ -32,21 +32,36 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@Component("myDaoRegistry")
public class DaoRegistry implements ApplicationContextAware {
private ApplicationContext myAppCtx;
@Autowired
private FhirContext myContext;
/**
* Constructor
*/
public DaoRegistry() {
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) {
supportedResourceTypes.addAll(theSupportedResourceTypes);
}
mySupportedResourceTypes = supportedResourceTypes;
myResourceNameToResourceDao = null;
}
@Override
public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
@ -62,6 +77,9 @@ public class DaoRegistry implements ApplicationContextAware {
return retVal;
}
/**
* @throws InvalidRequestException If the given resource type is not supported
*/
public IFhirResourceDao getResourceDao(String theResourceName) {
init();
IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName);
@ -84,7 +102,19 @@ public class DaoRegistry implements ApplicationContextAware {
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(Class<T> theResourceType) {
String resourceName = myContext.getResourceDefinition(theResourceType).getName();
return (IFhirResourceDao<T>) getResourceDao(resourceName);
try {
return (IFhirResourceDao<T>) getResourceDao(resourceName);
} catch (InvalidRequestException e) {
return null;
}
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(String theResourceType) {
try {
return (IFhirResourceDao<T>) getResourceDao(theResourceType);
} catch (InvalidRequestException e) {
return null;
}
}
private void init() {
@ -103,7 +133,9 @@ public class DaoRegistry implements ApplicationContextAware {
for (IFhirResourceDao nextResourceDao : theResourceDaos) {
RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType());
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
if (mySupportedResourceTypes == null || mySupportedResourceTypes.contains(nextResourceDef.getName())) {
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
}
}
}
@ -128,4 +160,16 @@ public class DaoRegistry implements ApplicationContextAware {
public IFhirResourceDao getSubscriptionDao() {
return getResourceDao(ResourceTypeEnum.SUBSCRIPTION.getCode());
}
public void setSupportedResourceTypes(String... theResourceTypes) {
setSupportedResourceTypes(toCollection(theResourceTypes));
}
private List<String> toCollection(String[] theResourceTypes) {
List<String> retVal = null;
if (theResourceTypes != null && theResourceTypes.length > 0) {
retVal = Arrays.asList(theResourceTypes);
}
return retVal;
}
}

View File

@ -27,11 +27,6 @@ import org.hl7.fhir.instance.model.api.IBaseBundle;
public class FhirResourceDaoMessageHeaderDstu2 extends FhirResourceDaoDstu2<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> {
@Override
public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
public static IBaseBundle throwProcessMessageNotImplemented() {
throw new NotImplementedOperationException("This operation is not yet implemented on this server");
}

View File

@ -60,6 +60,7 @@ import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.UrlUtil.UrlParts;
import com.google.common.collect.ArrayListMultimap;
import org.apache.http.NameValuePair;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
@ -665,7 +666,11 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}
//@formatter:off
@Override
public IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
/**
* Transaction Order, per the spec:
@ -675,7 +680,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<Entry> {
@Override

View File

@ -81,7 +81,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
super();
}
private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<? extends IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) {
private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) {
if (theTerms == null) {
return;
}
@ -171,13 +171,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
/*
* Handle _content parameter (resource body content)
*/
List<List<? extends IQueryParameterType>> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT);
List<List<IQueryParameterType>> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT);
addTextSearch(qb, bool, contentAndTerms, "myContentText", "myContentTextEdgeNGram", "myContentTextNGram");
/*
* Handle _text parameter (resource narrative content)
*/
List<List<? extends IQueryParameterType>> textAndTerms = theParams.remove(Constants.PARAM_TEXT);
List<List<IQueryParameterType>> textAndTerms = theParams.remove(Constants.PARAM_TEXT);
addTextSearch(qb, bool, textAndTerms, "myNarrativeText", "myNarrativeTextEdgeNGram", "myNarrativeTextNGram");
if (theReferencingPid != null) {

View File

@ -1,7 +1,5 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
/*
@ -25,7 +23,5 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
*/
public interface IFhirResourceDaoMessageHeader<T extends IBaseResource> extends IFhirResourceDao<T> {
IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage);
// nothing right now
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nullable;
@ -60,6 +61,12 @@ public interface IFhirSystemDao<T, MT> extends IDao {
*/
MT metaGetOperation(RequestDetails theRequestDetails);
/**
* Implementations may implement this method to implement the $process-message
* operation
*/
IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage);
T transaction(RequestDetails theRequestDetails, T theResources);
}

View File

@ -20,9 +20,10 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import java.io.Closeable;
import java.util.Iterator;
public interface IResultIterator extends Iterator<Long> {
public interface IResultIterator extends Iterator<Long>, Closeable {
int getSkippedCount();

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.param.DateRangeParam;
@ -34,7 +35,7 @@ import java.util.Set;
public interface ISearchBuilder {
IResultIterator createQuery(SearchParameterMap theParams, String theSearchUuid);
IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime);
void setMaxResultsToFetch(Integer theMaxResultsToFetch);

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@ -64,7 +65,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceException;
import java.util.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*;
@ -85,6 +88,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
private MatchUrlService myMatchUrlService;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired(required = false)
private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect;
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) {
@ -451,7 +456,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
}
transactionStopWatch.endCurrentTask();
ourLog.info("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations());
ourLog.debug("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations());
return response;
}
@ -576,8 +581,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
*/
for (int i = 0; i < theEntries.size(); i++) {
if (i % 100 == 0) {
ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size());
if (i % 250 == 0) {
ourLog.info("Processed {} non-GET entries out of {} in transaction", i, theEntries.size());
}
BUNDLEENTRY nextReqEntry = theEntries.get(i);
@ -749,7 +754,13 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
int i = 0;
for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
if (i++ % 250 == 0) {
ourLog.info("Have indexed {} entities out of {} in transaction", i, theIdToPersistedOutcome.values().size());
}
IBaseResource nextResource = nextOutcome.getResource();
if (nextResource == null) {
continue;
@ -803,7 +814,16 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
theTransactionStopWatch.endCurrentTask();
theTransactionStopWatch.startTask("Flush writes to database");
flushJpaSession();
try {
flushJpaSession();
} catch (PersistenceException e) {
if (myHapiFhirHibernateJpaDialect != null) {
List<String> types = theIdToPersistedOutcome.keySet().stream().filter(t -> t != null).map(t -> t.getResourceType()).collect(Collectors.toList());
String message = "Error flushing transaction with resource types: " + types;
throw myHapiFhirHibernateJpaDialect.translate(e, message);
}
throw e;
}
theTransactionStopWatch.endCurrentTask();
if (conditionalRequestUrls.size() > 0) {

View File

@ -30,16 +30,15 @@ import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f FROM ForcedId f WHERE myForcedId = :forced_id")
public List<ForcedId> findByForcedId(@Param("forced_id") String theForcedId);
@Query("SELECT f FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
public List<ForcedId> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
// FIXME: JA We should log a performance warning if this is used since it's not indexed
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)")
List<Long> findByForcedId(@Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN (:forced_id)")
List<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid in (:pids)")
Collection<ForcedId> findByResourcePids(@Param("pids") Collection<Long> pids);
}

View File

@ -1,10 +1,6 @@
package ca.uhn.fhir.jpa.dao.data;
import java.util.Collection;
import java.util.Date;
import javax.persistence.TemporalType;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
@ -13,7 +9,9 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.Temporal;
import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import javax.persistence.TemporalType;
import java.util.Collection;
import java.util.Date;
/*
* #%L
@ -74,6 +72,13 @@ public interface IResourceHistoryTableDao extends JpaRepository<ResourceHistoryT
@Query("SELECT t.myId FROM ResourceHistoryTable t WHERE t.myResourceId = :resId AND t.myResourceVersion != :dontWantVersion")
Slice<Long> findForResourceId(Pageable thePage, @Param("resId") Long theId, @Param("dontWantVersion") Long theDontWantVersion);
@Query("" +
"SELECT v.myId FROM ResourceHistoryTable v " +
"LEFT OUTER JOIN ResourceTable t ON (v.myResourceId = t.myId) " +
"WHERE v.myResourceVersion != t.myVersion AND " +
"t.myId = :resId")
Slice<Long> findIdsOfPreviousVersionsOfResourceId(Pageable thePage, @Param("resId") Long theResourceId);
@Query("" +
"SELECT v.myId FROM ResourceHistoryTable v " +
"LEFT OUTER JOIN ResourceTable t ON (v.myResourceId = t.myId) " +

View File

@ -33,6 +33,6 @@ import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
public interface ISearchParamPresentDao extends JpaRepository<SearchParamPresent, Long> {
@Query("SELECT s FROM SearchParamPresent s WHERE s.myResource = :res")
public Collection<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource);
Collection<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource);
}

View File

@ -20,17 +20,9 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* #L%
*/
import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.dstu3.model.MessageHeader;
import org.hl7.fhir.instance.model.api.IBaseBundle;
public class FhirResourceDaoMessageHeaderDstu3 extends FhirResourceDaoDstu3<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> {
@Override
public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
// nothing right now
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
*/
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
@ -29,6 +30,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetai
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -79,6 +81,11 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return retVal;
}
@Override
public IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
@Transactional(propagation = Propagation.NEVER)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
@ -86,4 +93,5 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
}
}

View File

@ -89,6 +89,14 @@ public class JpaValidationSupportDstu3 implements IJpaValidationSupportDstu3, Ap
return fetchResource(theCtx, CodeSystem.class, theSystem);
}
@Override
public ValueSet fetchValueSet(FhirContext theCtx, String theSystem) {
if (isBlank(theSystem)) {
return null;
}
return fetchResource(theCtx, ValueSet.class, theSystem);
}
@SuppressWarnings("unchecked")
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {

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