Merge remote-tracking branch 'origin/master' into ng_20201218_survivorship_poc

This commit is contained in:
Nick Goupinets 2021-01-26 09:18:20 -05:00
commit 04db4ed229
45 changed files with 2386 additions and 1025 deletions

View File

@ -3,10 +3,17 @@ HAPI FHIR
HAPI FHIR - Java API for HL7 FHIR Clients and Servers
[![Build Status](https://dev.azure.com/hapifhir/HAPI%20FHIR/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/hapifhir/HAPI%20FHIR/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg)](https://codecov.io/gh/jamesagnew/hapi-fhir)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg)](http://search.maven.org/#search|ga|1|ca.uhn.hapi.fhir)
[![License](https://img.shields.io/badge/license-apache%202.0-60C060.svg)](https://hapifhir.io/hapi-fhir/license.html)
[![License][Badge-License]][Link-License]
## CI/CD
| CI Status (master) | SNAPSHOT Pipeline | Current Release |
| :---: | :---: | :---: |
| [![Build Status][Badge-AzurePipelineMaster]][Link-AzurePipelinesMaster] | [![Build Status][Badge-AzureReleaseSnapshot]][Link-AzurePipelinesSnapshot] | [![Release Artifacts][Badge-MavenCentral]][Link-MavenCentral] |
## Test Coverage
[![codecov][Badge-CodeCov]][Link-CodeCov]
## Documentation and wiki
Complete project documentation is available here:
http://hapifhir.io
@ -16,4 +23,23 @@ http://hapi.fhir.org/
This project is Open Source, licensed under the Apache Software License 2.0.
Please see [this wiki page](https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help) for information on where to get help with HAPI FHIR. Please see [Smile CDR](https://smilecdr.com) for information on commercial support.
Please see [this wiki page][Link-wiki] for information on where to get help with HAPI FHIR.
Please see [Smile CDR][Link-SmileCDR] for information on commercial support.
[Link-AzurePipelines]: https://dev.azure.com/hapifhir/HAPI%20FHIR/_build
[Link-AzurePipelinesMaster]: https://dev.azure.com/hapifhir/HAPI%20FHIR/_build?definitionId=2
[Link-AzurePipelinesSnapshot]: https://dev.azure.com/hapifhir/HAPI%20FHIR/_build?definitionId=3
[Link-MavenCentral]: http://search.maven.org/#search|ga|1|ca.uhn.hapi.fhir
[Link-CodeCov]: https://codecov.io/gh/hapifhir/hapi-fhir
[Link-wiki]: https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help
[Link-SmileCDR]: https://smilecdr.com
[Link-License]: https://hapifhir.io/hapi-fhir/license.html
[Badge-AzurePipelineMaster]: https://dev.azure.com/hapifhir/HAPI%20FHIR/_apis/build/status/hapifhir.hapi-fhir?branchName=refs%2Fpull%2F2319%2Fmerge
[Badge-AzureReleaseSnapshot]: https://dev.azure.com/hapifhir/HAPI%20FHIR/_apis/build/status/SNAPSHOT%20pipeline?branchName=master
[Badge-MavenCentral]: https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg
[Badge-CodeCov]: https://codecov.io/gh/hapifhir/hapi-fhir/branch/master/graph/badge.svg?token=zHfnKfQB9X
[Badge-License]: https://img.shields.io/badge/license-apache%202.0-60C060.svg

View File

@ -99,14 +99,6 @@
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</dependency>
<dependency>
<groupId>com.helger</groupId>
<artifactId>ph-schematron</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
@ -151,6 +143,10 @@
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>

View File

@ -103,12 +103,17 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<classpathDependencyExcludes>
<dependencyExclude>org.slf4j:slf4j-android</dependencyExclude>
</classpathDependencyExcludes>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
<executions>
<execution>
<id>it</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<includes>

View File

@ -143,7 +143,7 @@ public class GenericClientDstu3IT {
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(idx).url().toString());
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(idx).url().toString());
idx++;
}
@ -178,7 +178,7 @@ public class GenericClientDstu3IT {
Request request = capt.getAllValues().get(0);
ourLog.info(request.headers().toString());
assertEquals("http://example.com/fhir/Binary?_format=json", request.url().toString());
assertEquals("http://example.com/fhir/Binary", request.url().toString());
validateUserAgent(capt);
assertEquals(Constants.CT_FHIR_JSON_NEW + ";charset=utf-8", request.body().contentType().toString().toLowerCase().replace(" ", ""));
@ -252,7 +252,7 @@ public class GenericClientDstu3IT {
assertNotNull(outcome.getResource());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">FINAL VALUE</div>", ((Patient) outcome.getResource()).getText().getDivAsString());
assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(0).url().toString());
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).url().toString());
}

View File

@ -0,0 +1,50 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* 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 IBaseInterceptorBroadcaster<POINTCUT extends IPointcut> {
/**
* 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);
/**
* Does this broadcaster have any hooks for the given pointcut?
*
* @param thePointcut The poointcut
* @return Does this broadcaster have any hooks for the given pointcut?
* @since 4.0.0
*/
boolean hasHooks(POINTCUT thePointcut);
}

View File

@ -0,0 +1,94 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* 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;
import java.util.function.Predicate;
public interface IBaseInterceptorService<POINTCUT extends IPointcut> extends IBaseInterceptorBroadcaster<POINTCUT> {
/**
* 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
* @return Returns <code>true</code> if the interceptor was found and removed
*/
boolean unregisterInterceptor(Object 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);
/**
* Unregisters all interceptors that are indicated by the given callback function returning <code>true</code>
*/
void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction);
}

View File

@ -20,31 +20,6 @@ package ca.uhn.fhir.interceptor.api;
* #L%
*/
public interface IInterceptorBroadcaster {
public interface IInterceptorBroadcaster extends IBaseInterceptorBroadcaster<Pointcut> {
/**
* 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);
/**
* Does this broadcaster have any hooks for the given pointcut?
*
* @param thePointcut The poointcut
* @return Does this broadcaster have any hooks for the given pointcut?
* @since 4.0.0
*/
boolean hasHooks(Pointcut thePointcut);
}

View File

@ -20,80 +20,10 @@ package ca.uhn.fhir.interceptor.api;
* #L%
*/
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
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
* @return Returns <code>true</code> if the interceptor was found and removed
*/
boolean unregisterInterceptor(Object theInterceptor);
public interface IInterceptorService extends IBaseInterceptorService<Pointcut>, IInterceptorBroadcaster {
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);
/**
* Unregisters all interceptors that are indicated by the given callback function returning <code>true</code>
*/
void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction);
}

View File

@ -0,0 +1,17 @@
package ca.uhn.fhir.interceptor.api;
import javax.annotation.Nonnull;
import java.util.List;
public interface IPointcut {
@Nonnull
Class<?> getReturnType();
@Nonnull
List<String> getParameterTypes();
@Nonnull
String name();
boolean isShouldLogAndSwallowException(Throwable theException);
}

View File

@ -48,7 +48,7 @@ import java.util.Set;
* </ul>
* </p>
*/
public enum Pointcut {
public enum Pointcut implements IPointcut {
/**
* <b>Interceptor Framework Hook:</b>
@ -2178,6 +2178,7 @@ public enum Pointcut {
this(theReturnType, new ExceptionHandlingSpec(), theParameterTypes);
}
@Override
public boolean isShouldLogAndSwallowException(@Nonnull Throwable theException) {
for (Class<? extends Throwable> next : myExceptionHandlingSpec.myTypesToLogAndSwallow) {
if (next.isAssignableFrom(theException.getClass())) {
@ -2187,11 +2188,13 @@ public enum Pointcut {
return false;
}
@Override
@Nonnull
public Class<?> getReturnType() {
return myReturnType;
}
@Override
@Nonnull
public List<String> getParameterTypes() {
return myParameterTypes;

View File

@ -0,0 +1,621 @@
package ca.uhn.fhir.interceptor.executor;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* 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.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IBaseInterceptorService;
import ca.uhn.fhir.interceptor.api.IPointcut;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.ReflectionUtil;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public abstract class BaseInterceptorService<POINTCUT extends IPointcut> implements IBaseInterceptorService<POINTCUT>, IBaseInterceptorBroadcaster<POINTCUT> {
private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.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 BaseInterceptorService() {
this("default");
}
/**
* Constructor
*
* @param theName The name for this registry (useful for troubleshooting)
*/
public BaseInterceptorService(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;
}
public void setName(String theName) {
myName = theName;
}
protected void registerAnonymousInterceptor(POINTCUT thePointcut, Object theInterceptor, BaseInvoker theInvoker) {
Validate.notNull(thePointcut);
Validate.notNull(theInterceptor);
synchronized (myRegistryMutex) {
myAnonymousInvokers.put(thePointcut, theInvoker);
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 void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction) {
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
}
private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<POINTCUT, BaseInvoker> theGlobalInvokers) {
theGlobalInvokers.entries().removeIf(t -> theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
}
@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 boolean unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
boolean removed = myInterceptors.removeIf(t -> t == theInterceptor);
removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
return removed;
}
}
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;
return doCallHooks(thePointcut, theParams, null);
}
@Override
public boolean hasHooks(POINTCUT thePointcut) {
return myGlobalInvokers.containsKey(thePointcut)
|| myAnonymousInvokers.containsKey(thePointcut)
|| hasThreadLocalHooks(thePointcut);
}
private boolean hasThreadLocalHooks(POINTCUT thePointcut) {
ListMultimap<POINTCUT, BaseInvoker> hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null;
return hooks != null && hooks.containsKey(thePointcut);
}
@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);
Class<?> pointcutReturnType = thePointcut.getReturnType();
if (pointcutReturnType.equals(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 (pointcutReturnType.equals(void.class) == false) {
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 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) {
IPointcut nextPointcut = nextAddedHook.getPointcut();
if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) {
continue;
}
theInvokers.put((POINTCUT) nextPointcut, nextAddedHook);
}
// Make sure we're always sorted according to the order declared in
// @Order
for (IPointcut nextPointcut : theInvokers.keys()) {
List<BaseInvoker> nextInvokerList = theInvokers.get((POINTCUT) nextPointcut);
nextInvokerList.sort(Comparator.naturalOrder());
}
return addedInvokers;
}
protected 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 IPointcut myPointcut;
/**
* Constructor
*/
private HookInvoker(HookDescriptor theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
super(theInterceptor, theOrder);
myPointcut = theHook.getPointcut();
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 IPointcut 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];
if (nextParamType.equals(Pointcut.class)) {
args[i] = myPointcut;
} else {
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);
}
}
}
/**
* @return Returns a list of any added invokers
*/
private List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
ArrayList<HookInvoker> retVal = new ArrayList<>();
for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
Optional<HookDescriptor> hook = scanForHook(nextMethod);
if (hook.isPresent()) {
int methodOrder = theTypeOrder;
int methodOrderAnnotation = hook.get().getOrder();
if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) {
methodOrder = methodOrderAnnotation;
}
retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder));
}
}
return retVal;
}
protected abstract Optional<HookDescriptor> scanForHook(Method nextMethod);
protected 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(","));
}
protected static class HookDescriptor {
private final IPointcut myPointcut;
private final int myOrder;
HookDescriptor(IPointcut thePointcut, int theOrder) {
myPointcut = thePointcut;
myOrder = theOrder;
}
IPointcut getPointcut() {
return myPointcut;
}
int getOrder() {
return myOrder;
}
}
}

View File

@ -27,39 +27,13 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.ReflectionUtil;
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.function.Predicate;
import java.util.stream.Collectors;
import java.util.Optional;
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;
public class InterceptorService extends BaseInterceptorService<Pointcut> implements IInterceptorService, IInterceptorBroadcaster {
/**
* Constructor which uses a default name of "default"
@ -74,28 +48,14 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
* @param theName The name for this registry (useful for troubleshooting)
*/
public InterceptorService(String theName) {
super();
myName = theName;
super(theName);
}
/**
* Are threadlocal interceptors enabled on this registry (defaults to true)
*/
public boolean isThreadlocalInvokersEnabled() {
return myThreadlocalInvokersEnabled;
@Override
protected Optional<HookDescriptor> scanForHook(Method nextMethod) {
return findAnnotation(nextMethod, Hook.class).map(t -> new HookDescriptor(t.value(), t.order()));
}
/**
* 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
@ -103,309 +63,14 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
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);
}
}
BaseInvoker invoker = new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder);
registerAnonymousInterceptor(thePointcut, theInterceptor, invoker);
}
@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 void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction) {
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
}
private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<Pointcut, BaseInvoker> theGlobalInvokers) {
theGlobalInvokers.entries().removeIf(t->theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
}
@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 boolean unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
boolean removed = myInterceptors.removeIf(t -> t == theInterceptor);
removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
return removed;
}
}
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;
return doCallHooks(thePointcut, theParams, null);
}
@Override
public boolean hasHooks(Pointcut thePointcut) {
return myGlobalInvokers.containsKey(thePointcut)
|| myAnonymousInvokers.containsKey(thePointcut)
|| hasThreadLocalHooks(thePointcut);
}
private boolean hasThreadLocalHooks(Pointcut thePointcut) {
ListMultimap<Pointcut, BaseInvoker> hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null;
return hooks != null && hooks.containsKey(thePointcut);
}
@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);
Class<?> pointcutReturnType = thePointcut.getReturnType();
if (pointcutReturnType.equals(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 (pointcutReturnType.equals(void.class) == false) {
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;
@ -424,191 +89,5 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
}
}
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];
if (nextParamType.equals(Pointcut.class)) {
args[i] = myPointcut;
} else {
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 : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
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

@ -50,6 +50,7 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
* Constructor
*/
public DateParam() {
super();
}
/**

View File

@ -224,14 +224,6 @@
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

View File

@ -0,0 +1,5 @@
---
type: perf
issue: 2302
title: The JPA server generated SQL for date search parameters has been streamlined to avoid the use of
redundant OR expressions that slow down performance in some cases.

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 2309
title: "In the JPA server, HumanName.name.text was not being indexed and therefore was not searchable. This has been corrected."

View File

@ -56,37 +56,15 @@
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-searchparam</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<exclusions>
<exclusion>
<artifactId>xml-apis</artifactId>
<groupId>xml-apis</groupId>
</exclusion>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
</exclusions>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-searchparam</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.search</groupId>
@ -137,13 +115,13 @@
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- test dependencies -->
<!-- test dependencies -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>

View File

@ -373,43 +373,13 @@
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<exclusions>
<exclusion>
<artifactId>xml-apis</artifactId>
<groupId>xml-apis</groupId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
<!--<exclusion>-->
<!--<groupId>org.jboss.spec.javax.transaction</groupId>-->
<!--<artifactId>jboss-transaction-api_1.2_spec</artifactId>-->
<!--</exclusion>-->
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
@ -435,11 +405,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
@ -715,16 +680,6 @@
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb_runtime_version}</version>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<version>${jaxb_core_version}</version>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<version>${jaxb_core_version}</version>-->
<!--</dependency>-->
</dependencies>
</plugin>
<plugin>

View File

@ -1316,11 +1316,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theParams.setOffset(offset);
}
final Integer count = RestfulServerUtils.extractCountParameter(theRequest);
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
if (count != null) {
Integer maxPageSize = theRequest.getServer().getMaximumPageSize();
if (maxPageSize != null) {
Validate.inclusiveBetween(1, theRequest.getServer().getMaximumPageSize(), count, "Count must be positive integer and less than " + maxPageSize);
if (maxPageSize != null && count > maxPageSize) {
ourLog.info("Reducing {} from {} to {} which is the maximum allowable page size.", Constants.PARAM_COUNT, count, maxPageSize);
count = maxPageSize;
}
theParams.setCount(count);
} else if (theRequest.getServer().getDefaultPageSize() != null) {

View File

@ -126,6 +126,9 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
if (theParam instanceof DateParam) {
DateParam date = (DateParam) theParam;
if (!date.isEmpty()) {
if (theOperation == SearchFilterParser.CompareOperation.ne) {
date = new DateParam(ParamPrefixEnum.EQUAL, date.getValueAsString());
}
DateRangeParam range = new DateRangeParam(date);
p = createPredicateDateFromRange(theBuilder,
theFrom,
@ -152,6 +155,7 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
return theDateParam == null || theDateParam.getPrecision().ordinal() == TemporalPrecisionEnum.DAY.ordinal();
}
@SuppressWarnings("unchecked")
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamDate> theFrom,
DateRangeParam theRange,
@ -191,36 +195,60 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
}
if (operation == SearchFilterParser.CompareOperation.lt) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
}
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
} else if (operation == SearchFilterParser.CompareOperation.le) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
}
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
} else if (operation == SearchFilterParser.CompareOperation.gt) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
}
lb = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
} else if (operation == SearchFilterParser.CompareOperation.ge) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
// use lower bound first
if (lowerBoundInstant != null) {
// the value has been reduced one in this case
lb = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
} else {
if (upperBoundInstant != null) {
ub = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound);
} else {
throw new InvalidRequestException("lowerBound and upperBound value not correctly specified for compare theOperation");
}
lb = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
} else if (operation == SearchFilterParser.CompareOperation.ne) {
}
} else if (operation == SearchFilterParser.CompareOperation.le) {
// use lower bound first
if (lowerBoundInstant != null) {
lb = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
} else {
if (upperBoundInstant != null) {
ub = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound);
} else {
throw new InvalidRequestException("lowerBound and upperBound value not correctly specified for compare theOperation");
}
}
} else if (operation == SearchFilterParser.CompareOperation.gt) {
// use upper bound first, e.g value between 6 and 10
// gt7 true, 10>7, gt11 false, 10>11 false, gt5 true, 10>5
if (upperBoundInstant != null) {
ub = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
} else {
if (lowerBoundInstant != null) {
lb = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound);
} else {
throw new InvalidRequestException("upperBound and lowerBound value not correctly specified for compare theOperation");
}
}
} else if (operation == SearchFilterParser.CompareOperation.ge) {
// use upper bound first, e.g value between 6 and 10
// gt7 true, 10>7, gt11 false, 10>11 false, gt5 true, 10>5
if (upperBoundInstant != null) {
ub = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);;
} else {
if (lowerBoundInstant != null) {
lb = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound);
} else {
throw new InvalidRequestException("upperBound and lowerBound value not correctly specified for compare theOperation");
}
}
} else if (operation == SearchFilterParser.CompareOperation.ne) {
if ((lowerBoundInstant == null) ||
(upperBoundInstant == null)) {
throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation");
}
lt = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
gt = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
lb = theBuilder.or(lt,
gt);
lb = theBuilder.or(lt, gt);
} else if ((operation == SearchFilterParser.CompareOperation.eq) || (operation == null)) {
if (lowerBoundInstant != null) {
gt = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);

View File

@ -102,6 +102,7 @@ import java.util.ListIterator;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.fromOperation;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
@ -267,12 +268,12 @@ class PredicateBuilderReference extends BasePredicateBuilder {
private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, From<?, ResourceLink> theJoin, List<Predicate> theCodePredicates, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
/*
* Which resource types can the given chained parameter actually link to? This might be a list
* where the chain is unqualified, as in: Observation?subject.identifier=(...)
* since subject can link to several possible target types.
*
* If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...)
* this is just a simple 1-entry list.
* Which resource types can the given chained parameter actually link to? This might be a list
* where the chain is unqualified, as in: Observation?subject.identifier=(...)
* since subject can link to several possible target types.
*
* If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...)
* this is just a simple 1-entry list.
*/
final List<Class<? extends IBaseResource>> resourceTypes = determineCandidateResourceTypesForChain(theResourceName, theParamName, theReferenceParam);
@ -593,7 +594,14 @@ class PredicateBuilderReference extends BasePredicateBuilder {
switch (nextParamDef.getParamType()) {
case DATE:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
myPredicateBuilder.addPredicateDate(theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId);
// FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt'
// to create the predicateDate instead of generic one with operation = null
SearchFilterParser.CompareOperation operation = null;
if (nextAnd.size() > 0) {
DateParam param = (DateParam) nextAnd.get(0);
operation = ca.uhn.fhir.jpa.search.builder.QueryStack.toOperation(param.getPrefix());
}
myPredicateBuilder.addPredicateDate(theResourceName, nextParamDef, nextAnd, operation, theRequestPartitionId);
}
break;
case QUANTITY:
@ -716,67 +724,58 @@ class PredicateBuilderReference extends BasePredicateBuilder {
private Predicate processFilterParameter(SearchFilterParser.FilterParameter theFilter,
String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
if (theFilter.getParamPath().getName().equals(Constants.PARAM_SOURCE)) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null, null, null, theFilter.getValue());
return addPredicateSource(Collections.singletonList(param), theFilter.getOperation(), theRequest);
} else if (theFilter.getParamPath().getName().equals(IAnyResource.SP_RES_ID)) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null,
null,
null,
theFilter.getValue());
return myPredicateBuilder.addPredicateResourceId(Collections.singletonList(Collections.singletonList(param)), myResourceName, theFilter.getOperation(), theRequestPartitionId);
} else if (theFilter.getParamPath().getName().equals(IAnyResource.SP_RES_LANGUAGE)) {
return addPredicateLanguage(Collections.singletonList(Collections.singletonList(new StringParam(theFilter.getValue()))),
theFilter.getOperation());
}
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, theFilter.getParamPath().getName());
if (searchParam == null) {
throw new InvalidRequestException("Invalid search parameter specified, " + theFilter.getParamPath().getName() + ", for resource type " + theResourceName);
} else if (searchParam.getName().equals(IAnyResource.SP_RES_ID)) {
if (searchParam.getParamType() == RestSearchParameterTypeEnum.TOKEN) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null,
null,
null,
theFilter.getValue());
return myPredicateBuilder.addPredicateResourceId(Collections.singletonList(Collections.singletonList(param)), myResourceName, theFilter.getOperation(), theRequestPartitionId);
} else {
throw new InvalidRequestException("Unexpected search parameter type encountered, expected token type for _id search");
}
} else if (searchParam.getName().equals(IAnyResource.SP_RES_LANGUAGE)) {
if (searchParam.getParamType() == RestSearchParameterTypeEnum.STRING) {
return addPredicateLanguage(Collections.singletonList(Collections.singletonList(new StringParam(theFilter.getValue()))),
theFilter.getOperation());
} else {
throw new InvalidRequestException("Unexpected search parameter type encountered, expected string type for language search");
}
} else if (searchParam.getName().equals(Constants.PARAM_SOURCE)) {
if (searchParam.getParamType() == RestSearchParameterTypeEnum.TOKEN) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null, null, null, theFilter.getValue());
return addPredicateSource(Collections.singletonList(param), theFilter.getOperation(), theRequest);
} else {
throw new InvalidRequestException("Unexpected search parameter type encountered, expected token type for _id search");
}
} else {
RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
if (typeEnum == RestSearchParameterTypeEnum.URI) {
return myPredicateBuilder.addPredicateUri(theResourceName, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
return myPredicateBuilder.addPredicateString(theResourceName, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
return myPredicateBuilder.addPredicateDate(theResourceName, searchParam, Collections.singletonList(new DateParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
return myPredicateBuilder.addPredicateNumber(theResourceName, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
String paramName = theFilter.getParamPath().getName();
SearchFilterParser.CompareOperation operation = theFilter.getOperation();
String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
String value = theFilter.getValue();
ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
return addPredicate(theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
return myPredicateBuilder.addPredicateQuantity(theResourceName, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
throw new InvalidRequestException("Composite search parameters not currently supported with _filter clauses");
} else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null,
null,
null,
theFilter.getValue());
return myPredicateBuilder.addPredicateToken(theResourceName, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId);
}
}
RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
if (typeEnum == RestSearchParameterTypeEnum.URI) {
return myPredicateBuilder.addPredicateUri(theResourceName, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
return myPredicateBuilder.addPredicateString(theResourceName, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
return myPredicateBuilder.addPredicateDate(theResourceName, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
return myPredicateBuilder.addPredicateNumber(theResourceName, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
String paramName = theFilter.getParamPath().getName();
SearchFilterParser.CompareOperation operation = theFilter.getOperation();
String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
String value = theFilter.getValue();
ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
return addPredicate(theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
return myPredicateBuilder.addPredicateQuantity(theResourceName, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
throw new InvalidRequestException("Composite search parameters not currently supported with _filter clauses");
} else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
TokenParam param = new TokenParam();
param.setValueAsQueryToken(null,
null,
null,
theFilter.getValue());
return myPredicateBuilder.addPredicateToken(theResourceName, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId);
}
return null;
}

View File

@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.search;
* #L%
*/
import com.sun.xml.bind.api.impl.NameConverter;
import org.apache.lucene.analysis.core.KeywordTokenizerFactory;
import org.apache.lucene.analysis.core.LowerCaseFilterFactory;
import org.apache.lucene.analysis.core.StopFilterFactory;
@ -35,7 +34,6 @@ import org.apache.lucene.analysis.standard.StandardTokenizerFactory;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurationContext;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* Factory for defining the analysers.

View File

@ -251,7 +251,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
ourLog.trace("Search entity marked as finished with {} results", search.getNumFound());
break;
}
if (search.getNumFound() >= theTo) {
if ((search.getNumFound() - search.getNumBlocked()) >= theTo) {
ourLog.trace("Search entity has {} results so far", search.getNumFound());
break;
}
@ -1091,7 +1091,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
int minWanted = 0;
if (myParams.getCount() != null) {
minWanted = myParams.getCount();
minWanted = Math.max(minWanted, myPagingProvider.getMaximumPageSize());
minWanted = Math.min(minWanted, myPagingProvider.getMaximumPageSize());
minWanted += currentlyLoaded;
}

View File

@ -89,6 +89,9 @@ import com.healthmarketscience.sqlbuilder.OrderObject;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.Subquery;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.tuple.Pair;
@ -112,12 +115,29 @@ import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class QueryStack {
private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class);
private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
static {
DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER);
ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix);
}
private final ModelConfig myModelConfig;
private final FhirContext myFhirContext;
private final SearchQueryBuilder mySqlBuilder;
@ -175,7 +195,6 @@ public class QueryStack {
mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending);
}
public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) {
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
NumberPredicateBuilder sortPredicateBuilder = mySqlBuilder.addNumberPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
@ -217,7 +236,6 @@ public class QueryStack {
mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
}
public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) {
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
StringPredicateBuilder sortPredicateBuilder = mySqlBuilder.addStringPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
@ -246,7 +264,6 @@ public class QueryStack {
mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValue(), theAscending);
}
@SuppressWarnings("unchecked")
private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder(PredicateBuilderTypeEnum theType, DbColumn theSourceJoinColumn, String theParamName, Supplier<T> theFactoryMethod) {
boolean cacheHit = false;
@ -299,7 +316,6 @@ public class QueryStack {
return orCondidtion;
}
private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId) {
switch (theParam.getParamType()) {
@ -310,7 +326,7 @@ public class QueryStack {
return createPredicateToken(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId);
}
case DATE: {
return createPredicateDate(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId);
return createPredicateDate(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId);
}
case QUANTITY: {
return createPredicateQuantity(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId);
@ -320,7 +336,6 @@ public class QueryStack {
throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParam.getParamType());
}
public Condition createPredicateCoords(@Nullable DbColumn theSourceJoinColumn,
String theResourceName,
RuntimeSearchParam theSearchParam,
@ -363,7 +378,7 @@ public class QueryStack {
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theResourceName, paramName, predicateBuilder, theOperation, theRequestPartitionId);
Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, predicateBuilder, theOperation);
codePredicates.add(p);
}
@ -429,7 +444,7 @@ public class QueryStack {
} else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
return theQueryStack3.createPredicateString(null, theResourceName, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
return theQueryStack3.createPredicateDate(null, theResourceName, searchParam, Collections.singletonList(new DateParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
return theQueryStack3.createPredicateDate(null, theResourceName, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
return theQueryStack3.createPredicateNumber(null, theResourceName, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
@ -976,7 +991,15 @@ public class QueryStack {
switch (nextParamDef.getParamType()) {
case DATE:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId));
// FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt'
// to create the predicateDate instead of generic one with operation = null
SearchFilterParser.CompareOperation operation = null;
if (nextAnd.size() > 0) {
DateParam param = (DateParam) nextAnd.get(0);
operation = toOperation(param.getPrefix());
}
andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, operation, theRequestPartitionId));
//andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId));
}
break;
case QUANTITY:
@ -1204,29 +1227,19 @@ public class QueryStack {
}
public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) {
if (thePrefix != null) {
switch (thePrefix) {
case APPROXIMATE:
return SearchFilterParser.CompareOperation.ap;
case EQUAL:
return SearchFilterParser.CompareOperation.eq;
case GREATERTHAN:
return SearchFilterParser.CompareOperation.gt;
case GREATERTHAN_OR_EQUALS:
return SearchFilterParser.CompareOperation.ge;
case LESSTHAN:
return SearchFilterParser.CompareOperation.lt;
case LESSTHAN_OR_EQUALS:
return SearchFilterParser.CompareOperation.le;
case NOT_EQUAL:
return SearchFilterParser.CompareOperation.ne;
case ENDS_BEFORE:
return SearchFilterParser.CompareOperation.eb;
case STARTS_AFTER:
return SearchFilterParser.CompareOperation.sa;
}
SearchFilterParser.CompareOperation retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.getKey(thePrefix);
}
return SearchFilterParser.CompareOperation.eq;
return defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq);
}
public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) {
ParamPrefixEnum retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.get(thePrefix);
}
return defaultIfNull(retVal, ParamPrefixEnum.EQUAL);
}
private static String getChainedPart(String parameter) {

View File

@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
@ -64,16 +63,16 @@ public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder {
public Condition createPredicateDateWithoutIdentityPredicate(IQueryParameterType theParam,
String theResourceName,
String theParamName,
DatePredicateBuilder theFrom,
SearchFilterParser.CompareOperation theOperation,
RequestPartitionId theRequestPartitionId) {
SearchFilterParser.CompareOperation theOperation) {
Condition p;
if (theParam instanceof DateParam) {
DateParam date = (DateParam) theParam;
if (!date.isEmpty()) {
if (theOperation == SearchFilterParser.CompareOperation.ne) {
date = new DateParam(ParamPrefixEnum.EQUAL, date.getValueAsString());
}
DateRangeParam range = new DateRangeParam(date);
p = createPredicateDateFromRange(theFrom, range, theOperation);
} else {
@ -96,6 +95,8 @@ public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder {
private Condition createPredicateDateFromRange(DatePredicateBuilder theFrom,
DateRangeParam theRange,
SearchFilterParser.CompareOperation theOperation) {
Date lowerBoundInstant = theRange.getLowerBoundAsInstant();
Date upperBoundInstant = theRange.getUpperBoundAsInstant();
@ -103,16 +104,17 @@ public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder {
DateParam upperBound = theRange.getUpperBound();
Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger();
Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger();
Comparable genericLowerBound;
Comparable genericUpperBound;
/**
Comparable<?> genericLowerBound;
Comparable<?> genericUpperBound;
/*
* If all present search parameters are of DAY precision, and {@link ca.uhn.fhir.jpa.model.entity.ModelConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true,
* then we attempt to use the ordinal field for date comparisons instead of the date field.
*/
boolean isOrdinalComparison = isNullOrDayPrecision(lowerBound) && isNullOrDayPrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches();
Condition lt;
Condition gt = null;
Condition gt;
Condition lb = null;
Condition ub = null;
DatePredicateBuilder.ColumnEnum lowValueField;
@ -130,28 +132,24 @@ public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder {
genericUpperBound = upperBoundInstant;
}
if (theOperation == SearchFilterParser.CompareOperation.lt) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare theOperation");
if (theOperation == SearchFilterParser.CompareOperation.lt || theOperation == SearchFilterParser.CompareOperation.le) {
// use lower bound first
if (lowerBoundInstant != null) {
lb = theFrom.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericLowerBound);
} else if (upperBoundInstant != null) {
ub = theFrom.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound);
} else {
throw new InvalidRequestException("lowerBound and upperBound value not correctly specified for comparing " + theOperation);
}
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theFrom.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN, genericLowerBound);
} else if (theOperation == SearchFilterParser.CompareOperation.le) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare theOperation");
} else if (theOperation == SearchFilterParser.CompareOperation.gt || theOperation == SearchFilterParser.CompareOperation.ge) {
// use upper bound first, e.g value between 6 and 10
if (upperBoundInstant != null) {
ub = theFrom.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericUpperBound);
} else if (lowerBoundInstant != null) {
lb = theFrom.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound);
} else {
throw new InvalidRequestException("upperBound and lowerBound value not correctly specified for compare theOperation");
}
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theFrom.createPredicate(highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound);
} else if (theOperation == SearchFilterParser.CompareOperation.gt) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare theOperation");
}
lb = theFrom.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN, genericUpperBound);
} else if (theOperation == SearchFilterParser.CompareOperation.ge) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare theOperation");
}
lb = theFrom.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound);
} else if (theOperation == SearchFilterParser.CompareOperation.ne) {
if ((lowerBoundInstant == null) ||
(upperBoundInstant == null)) {
@ -160,7 +158,10 @@ public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder {
lt = theFrom.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN, genericLowerBound);
gt = theFrom.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN, genericUpperBound);
lb = ComboCondition.or(lt, gt);
} else if ((theOperation == SearchFilterParser.CompareOperation.eq) || (theOperation == null)) {
} else if ((theOperation == SearchFilterParser.CompareOperation.eq)
|| (theOperation == SearchFilterParser.CompareOperation.sa)
|| (theOperation == SearchFilterParser.CompareOperation.eb)
|| (theOperation == null)) {
if (lowerBoundInstant != null) {
gt = theFrom.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound);
lt = theFrom.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound);

View File

@ -140,7 +140,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
}
}
assertEquals(myObservationIdsEvenOnly.subList(10, 25), returnedIdValues, "Wrong response from " + outcome.getClass());
assertEquals(2, hitCount.get());
assertEquals(3, hitCount.get());
}

View File

@ -703,7 +703,9 @@ public class FhirResourceDaoR4FilterTest extends BaseJpaR4Test {
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Constants.PARAM_FILTER, new StringParam("birthdate gt 1955-01-01"));
myCaptureQueriesListener.clear();
found = toUnqualifiedVersionlessIdValues(myPatientDao.search(map));
myCaptureQueriesListener.logSelectQueriesForCurrentThread(0);
assertThat(found, empty());
}
@ -723,7 +725,9 @@ public class FhirResourceDaoR4FilterTest extends BaseJpaR4Test {
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Constants.PARAM_FILTER, new StringParam("birthdate lt 1955-01-02"));
myCaptureQueriesListener.clear();
found = toUnqualifiedVersionlessIdValues(myPatientDao.search(map));
myCaptureQueriesListener.logSelectQueriesForCurrentThread(0);
assertThat(found, containsInAnyOrder(id1));
map = new SearchParameterMap();

View File

@ -4172,7 +4172,7 @@ public class FhirResourceDaoR4LegacySearchBuilderTest extends BaseJpaR4Test {
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search query:\n{}", searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "join"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "sp_value_low_date_ordinal>='20200605'"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "sp_value_high_date_ordinal>='20200605'"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "sp_value_low_date_ordinal<='20200606'"), searchQuery);
}
@ -4192,7 +4192,7 @@ public class FhirResourceDaoR4LegacySearchBuilderTest extends BaseJpaR4Test {
assertEquals(0, countMatches(searchQuery.toLowerCase(), "partition"), searchQuery);
assertEquals(2, countMatches(searchQuery.toLowerCase(), "join"), searchQuery);
assertEquals(2, countMatches(searchQuery.toLowerCase(), "hash_identity"), searchQuery);
assertEquals(4, countMatches(searchQuery.toLowerCase(), "sp_value_low"), searchQuery);
assertEquals(2, countMatches(searchQuery.toLowerCase(), "sp_value_low"), searchQuery);
}
// Period search

View File

@ -4369,7 +4369,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search query:\n{}", searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "join"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "t0.sp_value_low_date_ordinal >= '20200605'"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "t0.sp_value_high_date_ordinal >= '20200605'"), searchQuery);
assertEquals(1, countMatches(searchQuery.toLowerCase(), "t0.sp_value_low_date_ordinal <= '20200606'"), searchQuery);
}
@ -4389,7 +4389,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
assertEquals(0, countMatches(searchQuery.toLowerCase(), "partition"), searchQuery);
assertEquals(2, countMatches(searchQuery.toLowerCase(), "join"), searchQuery);
assertEquals(2, countMatches(searchQuery.toLowerCase(), "hash_identity"), searchQuery);
assertEquals(4, countMatches(searchQuery.toLowerCase(), "sp_value_low"), searchQuery);
// - query is changed 'or' is removed
assertEquals(2, countMatches(searchQuery.toLowerCase(), "sp_value_low"), searchQuery);
}
// Period search

View File

@ -1702,7 +1702,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
// NOTE: the query is changed, only one SP_VALUE_LOW and SP_VALUE_HIGH
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_HIGH"));
}
@ -1783,7 +1785,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
// NOTE: the query is changed, only one SP_VALUE_LOW and SP_VALUE_HIGH
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_HIGH"));
}
@ -1861,7 +1865,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
// NOTE: the query is changed, only one SP_VALUE_LOW and SP_VALUE_HIGH
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_HIGH"));
}

View File

@ -30,6 +30,7 @@ import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
@ -50,6 +51,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -82,6 +85,19 @@ public class SearchParamExtractorR4Test {
assertEquals("CODE", token.getValue());
}
@Test
public void testName() {
Patient patient = new Patient();
HumanName humanName = patient.addName();
humanName.addGiven("Jimmy");
humanName.setFamily("Jones");
humanName.setText("Jimmy Jones");
SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, mySearchParamRegistry);
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> stringSearchParams = extractor.extractSearchParamStrings(patient);
List<String> nameValues = stringSearchParams.stream().filter(param -> "name".equals(param.getParamName())).map(ResourceIndexedSearchParamString::getValueExact).collect(Collectors.toList());
assertThat(nameValues, containsInAnyOrder("Jimmy", "Jones", "Jimmy Jones"));
}
@Test
public void testTokenOnSearchParamContext() {
SearchParameter sp = new SearchParameter();

View File

@ -65,6 +65,7 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -126,6 +127,7 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
.execute();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, result);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertThat(returnedIdValues, hasSize(15));
assertEquals(myObservationIdsEvenOnly.subList(0, 15), returnedIdValues);
// Fetch the next page
@ -135,6 +137,7 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertThat(returnedIdValues, hasSize(10));
assertEquals(myObservationIdsEvenOnly.subList(15, 25), returnedIdValues);
}

View File

@ -13,8 +13,6 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.collect.Lists;
@ -52,7 +50,6 @@ import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@ -428,23 +425,4 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
}
}
public static void verifyDaoInterceptor(IServerInterceptor theDaoInterceptor) {
ArgumentCaptor<ActionRequestDetails> ardCaptor;
ArgumentCaptor<RestOperationTypeEnum> opTypeCaptor;
ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(theDaoInterceptor, atLeast(1)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
// boolean good = false;
// for (int i = 0; i < opTypeCaptor.getAllValues().size(); i++) {
// if (RestOperationTypeEnum.CREATE.equals(opTypeCaptor.getAllValues().get(i))) {
// if ("Patient".equals(ardCaptor.getValue().getResourceType())) {
// if (ardCaptor.getValue().getResource() != null) {
// good = true;
// }
// }
// }
// }
// assertTrue(good);
}
}

View File

@ -12,16 +12,16 @@
<artifactId>hapi-fhir-jpaserver-model</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR Model</name>
<name>HAPI FHIR JPA Model</name>
<dependencies>
<!-- FHIR -->
<dependency>
<groupId>org.fhir</groupId>
<artifactId>ucum</artifactId>
</dependency>
<groupId>org.fhir</groupId>
<artifactId>ucum</artifactId>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
@ -71,10 +71,6 @@
<artifactId>xml-apis</artifactId>
<groupId>xml-apis</groupId>
</exclusion>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
@ -83,6 +79,10 @@
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>

View File

@ -132,6 +132,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private BaseRuntimeChildDefinition myDurationValueValueChild;
private BaseRuntimeChildDefinition myHumanNameFamilyValueChild;
private BaseRuntimeChildDefinition myHumanNameGivenValueChild;
private BaseRuntimeChildDefinition myHumanNameTextValueChild;
private BaseRuntimeChildDefinition myContactPointValueValueChild;
private BaseRuntimeChildDefinition myIdentifierSystemValueChild;
private BaseRuntimeChildDefinition myIdentifierValueValueChild;
@ -877,6 +878,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
for (String next : givens) {
createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, next);
}
List<String> texts = extractValuesAsStrings(myHumanNameTextValueChild, theValue);
for (String next : texts) {
createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, next);
}
}
private void addString_Quantity(String theResourceType, Set<ResourceIndexedSearchParamString> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
@ -1138,6 +1143,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
BaseRuntimeElementCompositeDefinition<?> humanNameDefinition = (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("HumanName");
myHumanNameFamilyValueChild = humanNameDefinition.getChildByName("family");
myHumanNameGivenValueChild = humanNameDefinition.getChildByName("given");
myHumanNameTextValueChild = humanNameDefinition.getChildByName("text");
BaseRuntimeElementCompositeDefinition<?> contactPointDefinition = (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("ContactPoint");
myContactPointValueValueChild = contactPointDefinition.getChildByName("value");

View File

@ -675,10 +675,16 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
/**
* Sets the paging provider to use, or <code>null</code> to use no paging (which is the default)
* Sets the paging provider to use, or <code>null</code> to use no paging (which is the default).
* This will set defaultPageSize and maximumPageSize from the paging provider.
*/
public void setPagingProvider(IPagingProvider thePagingProvider) {
myPagingProvider = thePagingProvider;
if (myPagingProvider != null) {
setDefaultPageSize(myPagingProvider.getDefaultPageSize());
setMaximumPageSize(myPagingProvider.getMaximumPageSize());
}
}
@Override

View File

@ -235,16 +235,6 @@
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

View File

@ -125,16 +125,6 @@
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

View File

@ -197,23 +197,6 @@
<optional>true</optional>
</dependency>
<!--
<dependency>
<groupId>xpp3</groupId>
<artifactId>xpp3_min</artifactId>
<version>1.1.4c</version>
<optional>true</optional>
</dependency>
-->
<!--
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>9.6.0-4</version>
</dependency>
-->
<!-- Used by the validator -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@ -292,27 +275,6 @@
<artifactId>javax.activation-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<scope>test</scope>
</dependency>
<!-- UNIT TEST DEPENDENCIES -->
<dependency>

View File

@ -176,16 +176,6 @@
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

View File

@ -245,16 +245,6 @@
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

View File

@ -268,16 +268,6 @@
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-core</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.sun.xml.bind</groupId>-->
<!--<artifactId>jaxb-impl</artifactId>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>

10
pom.xml
View File

@ -746,7 +746,7 @@
<jaxb_bundle_version>2.2.11_1</jaxb_bundle_version>
<jaxb_api_version>2.3.1</jaxb_api_version>
<jaxb_core_version>2.3.0.1</jaxb_core_version>
<jaxb_runtime_version>2.3.1</jaxb_runtime_version>
<jaxb_runtime_version>3.0.0</jaxb_runtime_version>
<jena_version>3.16.0</jena_version>
<jersey_version>3.0.0</jersey_version>
<!-- 9.4.17 seems to have issues -->
@ -892,12 +892,6 @@
<groupId>com.helger</groupId>
<artifactId>ph-schematron</artifactId>
<version>${ph_schematron_version}</version>
<exclusions>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.helger</groupId>
@ -917,7 +911,7 @@
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
<version>1.2.1</version>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>

View File

@ -85,5 +85,5 @@ steps:
inputs:
mavenPomFile: '$(System.DefaultWorkingDirectory)/pom.xml'
goals: deploy
options: '--settings $(System.DefaultWorkingDirectory)/settings.xml -P DIST,ALLMODULES'
publishJUnitResults: false
options: '--settings $(System.DefaultWorkingDirectory)/settings.xml -P DIST,ALLMODULES -Dmaven.test.skip'
publishJUnitResults: false