reversed order matching operation bindings are searched so custom ope… (#3568)

* reversed order matching operation bindings are searched so custom operations override builtin ones

* change log

* add test

* reverse finding operations in interfaces and then classes so last-in-wins finds the right provider method
This commit is contained in:
Ken Stevens 2022-04-30 16:46:37 -04:00 committed by GitHub
parent 06030094c8
commit b59f2d05a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 23 deletions

View File

@ -0,0 +1,4 @@
type: change
issue: 3568
title: "Modified operation method binding so that later registered custom providers take precedence over earlier registered ones.
In particular, this means that custom operations can now override built-in operations."

View File

@ -20,13 +20,13 @@ package ca.uhn.fhir.rest.server;
* #L% * #L%
*/ */
import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.MethodMatchEnum; import ca.uhn.fhir.rest.server.method.MethodMatchEnum;
import java.util.LinkedList;
import java.util.List;
/** /**
* Holds all method bindings for an individual resource type * Holds all method bindings for an individual resource type
*/ */
@ -35,7 +35,7 @@ public class ResourceBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceBinding.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceBinding.class);
private String resourceName; private String resourceName;
private List<BaseMethodBinding<?>> myMethodBindings = new ArrayList<>(); private LinkedList<BaseMethodBinding<?>> myMethodBindings = new LinkedList<>();
/** /**
* Constructor * Constructor
@ -44,14 +44,6 @@ public class ResourceBinding {
super(); super();
} }
/**
* Constructor
*/
public ResourceBinding(String resourceName, List<BaseMethodBinding<?>> methods) {
this.resourceName = resourceName;
this.myMethodBindings = methods;
}
public BaseMethodBinding<?> getMethod(RequestDetails theRequest) { public BaseMethodBinding<?> getMethod(RequestDetails theRequest) {
if (null == myMethodBindings) { if (null == myMethodBindings) {
ourLog.warn("No methods exist for resource: {}", resourceName); ourLog.warn("No methods exist for resource: {}", resourceName);
@ -95,12 +87,8 @@ public class ResourceBinding {
return myMethodBindings; return myMethodBindings;
} }
public void setMethods(List<BaseMethodBinding<?>> methods) {
this.myMethodBindings = methods;
}
public void addMethod(BaseMethodBinding<?> method) { public void addMethod(BaseMethodBinding<?> method) {
this.myMethodBindings.add(method); this.myMethodBindings.push(method);
} }
@Override @Override

View File

@ -20,12 +20,12 @@ package ca.uhn.fhir.rest.server;
* #L% * #L%
*/ */
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.api.AddProfileTagEnum; import ca.uhn.fhir.context.api.AddProfileTagEnum;
import ca.uhn.fhir.context.api.BundleInclusionRule; import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
@ -437,14 +437,14 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
Class<?> clazz = theProvider.getClass(); Class<?> clazz = theProvider.getClass();
Class<?> supertype = clazz.getSuperclass(); Class<?> supertype = clazz.getSuperclass();
while (!Object.class.equals(supertype)) { while (!Object.class.equals(supertype)) {
count += findResourceMethods(theProvider, supertype);
count += findResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces()); count += findResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces());
count += findResourceMethods(theProvider, supertype);
supertype = supertype.getSuperclass(); supertype = supertype.getSuperclass();
} }
try { try {
count += findResourceMethods(theProvider, clazz);
count += findResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces()); count += findResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces());
count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
throw new ConfigurationException(Msg.code(288) + "Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e); throw new ConfigurationException(Msg.code(288) + "Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
} }
@ -456,8 +456,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
private int findResourceMethodsOnInterfaces(Object theProvider, Class<?>[] interfaces) { private int findResourceMethodsOnInterfaces(Object theProvider, Class<?>[] interfaces) {
int count = 0; int count = 0;
for (Class<?> anInterface : interfaces) { for (Class<?> anInterface : interfaces) {
count += findResourceMethods(theProvider, anInterface);
count += findResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces()); count += findResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces());
count += findResourceMethods(theProvider, anInterface);
} }
return count; return count;
} }

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ExtendWith(MockitoExtension.class)
class ResourceBindingTest {
@Mock
FhirContext ourFhirContext;
ResourceBinding myResourceBinding = new ResourceBinding();
@Test
public void testFILO() throws NoSuchMethodException {
// setup
Method method = ResourceBindingTest.class.getMethod("testFILO");
BaseMethodBinding<?> first = new PageMethodBinding(ourFhirContext, method);
BaseMethodBinding<?> second = new PageMethodBinding(ourFhirContext, method);;
// execute
myResourceBinding.addMethod(first);
myResourceBinding.addMethod(second);
// verify
List<BaseMethodBinding<?>> list = myResourceBinding.getMethodBindings();
assertNotEquals(first, second);
assertEquals(second, list.get(0));
assertEquals(first, list.get(1));
}
}

View File

@ -559,7 +559,7 @@ public class OperationServerDstu3Test {
servlet.setFhirContext(ourCtx); servlet.setFhirContext(ourCtx);
servlet.setResourceProviders(new PatientProvider()); servlet.setResourceProviders(new PatientProvider());
servlet.setPlainProviders(new PlainProvider()); servlet.registerProvider(new PlainProvider());
ServletHolder servletHolder = new ServletHolder(servlet); ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);
@ -676,7 +676,7 @@ public class OperationServerDstu3Test {
) { ) {
//@formatter:on //@formatter:on
ourLastMethod = "$OP_TYPE"; ourLastMethod = "$OP_TYPE_ONLY_STRING";
ourLastParam1 = theParam1; ourLastParam1 = theParam1;
Parameters retVal = new Parameters(); Parameters retVal = new Parameters();