Skip to content

Commit

Permalink
Issue-1077: Make CapabilityStatementProvider use the closest common s…
Browse files Browse the repository at this point in the history
…uperclass of provided resources when generating rest.resource.profile, instead of always using the base definition.
  • Loading branch information
Stig Rohde Døssing authored and srdo committed Oct 16, 2019
1 parent 9a05046 commit 8f65d21
Show file tree
Hide file tree
Showing 13 changed files with 665 additions and 194 deletions.
Expand Up @@ -46,6 +46,7 @@
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.util.ReflectionUtil;
import java.util.stream.Collectors;

/**
* This is the conformance provider for the jax rs servers. It requires all providers to be registered during startup because the conformance profile is generated during the postconstruct phase.
Expand Down Expand Up @@ -119,7 +120,8 @@ protected synchronized void setUpPostConstruct() {
return;
}

for (Entry<Class<? extends IResourceProvider>, IResourceProvider> provider : getProviders().entrySet()) {
ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> providers = getProviders();
for (Entry<Class<? extends IResourceProvider>, IResourceProvider> provider : providers.entrySet()) {
addProvider(provider.getValue(), provider.getKey());
}
List<BaseMethodBinding<?>> serverBindings = new ArrayList<BaseMethodBinding<?>>();
Expand All @@ -128,6 +130,7 @@ protected synchronized void setUpPostConstruct() {
}
serverConfiguration.setServerBindings(serverBindings);
serverConfiguration.setResourceBindings(new LinkedList<ResourceBinding>(myResourceNameToBinding.values()));
serverConfiguration.computeSharedSupertypeForResourcePerName(providers.values());
HardcodedServerAddressStrategy hardcodedServerAddressStrategy = new HardcodedServerAddressStrategy();
hardcodedServerAddressStrategy.setValue(getBaseForServer());
serverConfiguration.setServerAddressStrategy(hardcodedServerAddressStrategy);
Expand Down
@@ -1,106 +1,107 @@
package ca.uhn.fhir.jaxrs.server;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProvider;
import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProviderDstu3;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider;
import org.jboss.resteasy.specimpl.ResteasyHttpHeaders;
import org.junit.Before;
import org.junit.Test;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class AbstractJaxRsConformanceProviderDstu3Test {

private static final String BASEURI = "http://basiuri";
private static final String REQUESTURI = BASEURI + "/metadata";
AbstractJaxRsConformanceProvider provider;
private ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> providers;
private ResteasyHttpHeaders headers;
private MultivaluedHashMap<String, String> queryParameters;

@Before
public void setUp() throws Exception {
// uri info
queryParameters = new MultivaluedHashMap<>();
// headers
// headers = new ContainerRequest(new URI(BASEURI), new URI(REQUESTURI), HttpMethod.GET, null,
// new MapPropertiesDelegate());
headers = new ResteasyHttpHeaders(queryParameters);


providers = new ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider>();
provider = createConformanceProvider(providers);
}

@Test
public void testConformance() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsDummyPatientProvider.class, new TestJaxRsDummyPatientProvider());
Response response = createConformanceProvider(providers).conformance();
System.out.println(response);
}

@Test
public void testConformanceUsingOptions() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsDummyPatientProvider.class, new TestJaxRsDummyPatientProvider());
Response response = createConformanceProvider(providers).conformanceUsingOptions();
System.out.println(response);
}

@Test
public void testConformanceWithMethods() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsMockPatientRestProviderDstu3.class, new TestJaxRsMockPatientRestProviderDstu3());
Response response = createConformanceProvider(providers).conformance();
assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus());
assertTrue(response.getEntity().toString().contains("\"type\": \"Patient\""));
assertTrue(response.getEntity().toString().contains("\"someCustomOperation"));
System.out.println(response);
System.out.println(response.getEntity());
}

@Test
public void testConformanceInXml() throws Exception {
queryParameters.put(Constants.PARAM_FORMAT, Arrays.asList(Constants.CT_XML));
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsMockPatientRestProviderDstu3.class, new TestJaxRsMockPatientRestProviderDstu3());
Response response = createConformanceProvider(providers).conformance();
assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus());
System.out.println(response.getEntity());
assertTrue(response.getEntity().toString().contains(" <type value=\"Patient\"/>"));
assertTrue(response.getEntity().toString().contains("\"someCustomOperation"));
System.out.println(response.getEntity());
}

private AbstractJaxRsConformanceProvider createConformanceProvider(final ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> providers)
throws Exception {
AbstractJaxRsConformanceProvider result = new AbstractJaxRsConformanceProvider(FhirContext.forDstu3(), null, null, null) {
@Override
protected ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> getProviders() {
return providers;
}
};
// mocks
UriInfo uriInfo = mock(UriInfo.class);
when(uriInfo.getQueryParameters()).thenReturn(queryParameters);
when(uriInfo.getBaseUri()).thenReturn(new URI(BASEURI));
when(uriInfo.getRequestUri()).thenReturn(new URI(BASEURI + "/foo"));
result.setUriInfo(uriInfo);
result.setHeaders(headers);
result.setUpPostConstruct();
return result;
}

}
package ca.uhn.fhir.jaxrs.server;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProviderDstu3;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider;
import org.jboss.resteasy.specimpl.ResteasyHttpHeaders;
import org.junit.Before;
import org.junit.Test;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProviderDstu3;

public class AbstractJaxRsConformanceProviderDstu3Test {

private static final String BASEURI = "http://basiuri";
private static final String REQUESTURI = BASEURI + "/metadata";
AbstractJaxRsConformanceProvider provider;
private ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> providers;
private ResteasyHttpHeaders headers;
private MultivaluedHashMap<String, String> queryParameters;

@Before
public void setUp() throws Exception {
// uri info
queryParameters = new MultivaluedHashMap<>();
// headers
// headers = new ContainerRequest(new URI(BASEURI), new URI(REQUESTURI), HttpMethod.GET, null,
// new MapPropertiesDelegate());
headers = new ResteasyHttpHeaders(queryParameters);


providers = new ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider>();
provider = createConformanceProvider(providers);
}

@Test
public void testConformance() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsDummyPatientProviderDstu3.class, new TestJaxRsDummyPatientProviderDstu3());
Response response = createConformanceProvider(providers).conformance();
System.out.println(response);
}

@Test
public void testConformanceUsingOptions() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsDummyPatientProviderDstu3.class, new TestJaxRsDummyPatientProviderDstu3());
Response response = createConformanceProvider(providers).conformanceUsingOptions();
System.out.println(response);
}

@Test
public void testConformanceWithMethods() throws Exception {
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsMockPatientRestProviderDstu3.class, new TestJaxRsMockPatientRestProviderDstu3());
Response response = createConformanceProvider(providers).conformance();
assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus());
assertTrue(response.getEntity().toString().contains("\"type\": \"Patient\""));
assertTrue(response.getEntity().toString().contains("\"someCustomOperation"));
System.out.println(response);
System.out.println(response.getEntity());
}

@Test
public void testConformanceInXml() throws Exception {
queryParameters.put(Constants.PARAM_FORMAT, Arrays.asList(Constants.CT_XML));
providers.put(AbstractJaxRsConformanceProvider.class, provider);
providers.put(TestJaxRsMockPatientRestProviderDstu3.class, new TestJaxRsMockPatientRestProviderDstu3());
Response response = createConformanceProvider(providers).conformance();
assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus());
System.out.println(response.getEntity());
assertTrue(response.getEntity().toString().contains(" <type value=\"Patient\"/>"));
assertTrue(response.getEntity().toString().contains("\"someCustomOperation"));
System.out.println(response.getEntity());
}

private AbstractJaxRsConformanceProvider createConformanceProvider(final ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> providers)
throws Exception {
AbstractJaxRsConformanceProvider result = new AbstractJaxRsConformanceProvider(FhirContext.forDstu3(), null, null, null) {
@Override
protected ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> getProviders() {
return providers;
}
};
// mocks
UriInfo uriInfo = mock(UriInfo.class);
when(uriInfo.getQueryParameters()).thenReturn(queryParameters);
when(uriInfo.getBaseUri()).thenReturn(new URI(BASEURI));
when(uriInfo.getRequestUri()).thenReturn(new URI(BASEURI + "/foo"));
result.setUriInfo(uriInfo);
result.setHeaders(headers);
result.setUpPostConstruct();
return result;
}

}
@@ -0,0 +1,65 @@
package ca.uhn.fhir.rest.server;

import ca.uhn.fhir.model.api.annotation.ResourceDef;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import org.hl7.fhir.instance.model.api.IBaseResource;

/**
* <pre>
* When populating the StructureDefinition links in a capability statement,
* it can be useful to know the lowest common superclass for the profiles in use for a given resource name.
* This class finds this superclass, by incrementally computing the greatest common sequence of ancestor classes in the class hierarchies of registered resources.
* For instance, given the following classes
* MyPatient extends Patient
* MyPatient2 extends MyPatient
* MyPatient3 extends MyPatient
* MyPatient4 extends MyPatient3
* this class will find the common ancestor sequence "IBaseResource -> Patient -> MyPatient". MyPatient is the lowest common superclass in this hierarchy.
* </pre>
*
*/
public class CommonResourceSupertypeScanner {

private List<Class<? extends IBaseResource>> greatestSharedAncestorsDescending;
private boolean initialized;

/**
* Recomputes the lowest common superclass by adding a new resource definition to the hierarchy.
* @param resourceClass The resource class to add.
*/
public void register(Class<? extends IBaseResource> resourceClass) {
List<Class<? extends IBaseResource>> resourceClassesInHierarchy = new LinkedList<>();
Class<?> currentClass = resourceClass;
while (IBaseResource.class.isAssignableFrom(currentClass)
&& currentClass.getAnnotation(ResourceDef.class) != null) {
resourceClassesInHierarchy.add((Class<? extends IBaseResource>)currentClass);
currentClass = currentClass.getSuperclass();
}
Collections.reverse(resourceClassesInHierarchy);
if (initialized) {
for (int i = 0; i < Math.min(resourceClassesInHierarchy.size(), greatestSharedAncestorsDescending.size()); i++) {
if (greatestSharedAncestorsDescending.get(i) != resourceClassesInHierarchy.get(i)) {
greatestSharedAncestorsDescending = greatestSharedAncestorsDescending.subList(0, i);
break;
}
}
} else {
greatestSharedAncestorsDescending = resourceClassesInHierarchy;
initialized = true;
}
}

/**
* @return The lowest common superclass of currently registered resources.
*/
public Optional<Class<? extends IBaseResource>> getLowestCommonSuperclass() {
if (!initialized || greatestSharedAncestorsDescending.isEmpty()) {
return Optional.empty();
}
return Optional.ofNullable(greatestSharedAncestorsDescending.get(greatestSharedAncestorsDescending.size() - 1));
}

}
Expand Up @@ -9,9 +9,9 @@
* 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.
Expand All @@ -30,6 +30,7 @@
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.model.api.annotation.ProvidesResources;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Destroy;
Expand All @@ -45,6 +46,7 @@
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
Expand Down Expand Up @@ -209,6 +211,7 @@ public RestfulServerConfiguration createConfiguration() {
} catch (Exception e) {
// fall through
}
result.computeSharedSupertypeForResourcePerName(getResourceProviders());
return result;
}

Expand Down Expand Up @@ -932,8 +935,8 @@ protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest
preProcessedParams.add(HttpServletRequest.class, theRequest);
preProcessedParams.add(HttpServletResponse.class, theResponse);
if (!myInterceptorService.callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED, preProcessedParams)) {
return;
}
return;
}

String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath);

Expand Down Expand Up @@ -984,8 +987,8 @@ protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest
postProcessedParams.add(HttpServletRequest.class, theRequest);
postProcessedParams.add(HttpServletResponse.class, theResponse);
if (!myInterceptorService.callHooks(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED, postProcessedParams)) {
return;
}
return;
}

/*
* Actually invoke the server method. This call is to a HAPI method binding, which
Expand All @@ -1004,7 +1007,7 @@ protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest
myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY, hookParams);

ourLog.trace("Done writing to stream: {}", outputStreamOrWriter);
}
}

} catch (NotModifiedException | AuthenticationException e) {

Expand All @@ -1015,8 +1018,8 @@ protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest
handleExceptionParams.add(HttpServletResponse.class, theResponse);
handleExceptionParams.add(BaseServerResponseException.class, e);
if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) {
return;
}
return;
}

writeExceptionToResponse(theResponse, e);

Expand Down Expand Up @@ -1071,8 +1074,8 @@ protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest
handleExceptionParams.add(HttpServletResponse.class, theResponse);
handleExceptionParams.add(BaseServerResponseException.class, exception);
if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) {
return;
}
return;
}

/*
* If we're handling an exception, no summary mode should be applied
Expand Down

0 comments on commit 8f65d21

Please sign in to comment.