Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce embedded parameters for REST operations #6611

Draft
wants to merge 77 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
8537709
WIP
JPercival Oct 23, 2024
917d008
Merge remote-tracking branch 'origin/master' into ld-20250110-feature…
lukedegruchy Jan 10, 2025
b5094de
cdr boots but there's a runtime error when calling the rest method: …
lukedegruchy Jan 13, 2025
05abf62
Simple void method with OperationEmbeddedType works.
lukedegruchy Jan 14, 2025
a434400
Try to support setters in new params-type classes. Successfully call…
lukedegruchy Jan 14, 2025
18b08a7
Changes to set the table for parallel evaluate-measure and care-gaps …
lukedegruchy Jan 15, 2025
0e783d9
Add params classes.
lukedegruchy Jan 15, 2025
83a92b4
Cleanup MethodUtil. Replace OperationParam with OperationEmbeddedPar…
lukedegruchy Jan 15, 2025
b1e0091
Baby steps to refactoring OperationMethodBinding to support embedded …
lukedegruchy Jan 15, 2025
e4141e0
Spotless.
lukedegruchy Jan 15, 2025
f92abe3
Fix checkstyle with unique codes.
lukedegruchy Jan 15, 2025
78711a2
Fix checkstyle.
lukedegruchy Jan 15, 2025
a0d697e
Clone of $evaluate-measure works but in a really messy way.
lukedegruchy Jan 15, 2025
5804f16
Clone of $care-gaps works but in a really messy way. Revert changes…
lukedegruchy Jan 16, 2025
3d1dcc1
Eliminate duplicate methods and migrate the main methods to operation…
lukedegruchy Jan 16, 2025
b74f34c
Cleanup. New class for OperationMethodBinding.
lukedegruchy Jan 16, 2025
de882c2
Separate OperationIdParamDetails to use for OperationMethodBinding. …
lukedegruchy Jan 16, 2025
f1fb84f
Refactor BaseMethodBinding to be simpler and introduce a new class: …
lukedegruchy Jan 16, 2025
6375e56
Get rid of OperationEmbeddedType.
lukedegruchy Jan 16, 2025
3b2a565
Begin massive refactoring of MethodUtil#getResourceParameters. Spot…
lukedegruchy Jan 17, 2025
1b9a256
Distinguish between theMethod parameter and Method variable that's mu…
lukedegruchy Jan 17, 2025
dcbdc6b
Move closer to code reuse for MethodUtil but this isn't quite working…
lukedegruchy Jan 17, 2025
9a033cc
Fix algorithm to deal with RequestDetails passed as part of parameters.
lukedegruchy Jan 17, 2025
5786be2
Fix new algorithm to work with evaluteMeasure but careGaps is still b…
lukedegruchy Jan 17, 2025
c03e021
Get rid of use of OperationParams. Fix new algo so it passes startup…
lukedegruchy Jan 17, 2025
c964f0d
Fix bug with collection types. Kill old code.
lukedegruchy Jan 17, 2025
c29f0b4
Fix bug with collection types. Kill old code. Add basic unit tests.
lukedegruchy Jan 17, 2025
e823778
Write more tests and improve production class.
lukedegruchy Jan 17, 2025
23ae62b
Small cleanup.
lukedegruchy Jan 17, 2025
4fae950
Add more testing for method reflection classes.
lukedegruchy Jan 18, 2025
d221217
TODOs. Slight refactoring.
lukedegruchy Jan 18, 2025
c2bfbf8
Import FHIR structures for R4 and enhance tests to leverage FHIR clas…
lukedegruchy Jan 19, 2025
d4defac
Tweaks to existing tests and new test class.
lukedegruchy Jan 19, 2025
e52f6f9
Move tests to hapi-fhir-structures-r4 to circumvent a circular depend…
lukedegruchy Jan 20, 2025
483792e
Fix animal sniffer error.
lukedegruchy Jan 20, 2025
32bee05
Fix caregaps error by reusing OperationParameter.REQUEST_CONTENTS_USE…
lukedegruchy Jan 20, 2025
abca321
Fix bug with wrong boolean variable assignment.
lukedegruchy Jan 20, 2025
287ca14
Spotless.
lukedegruchy Jan 20, 2025
2aee179
Clean up a lot of TODOs. Test tweaks. First attempt at refactoring…
lukedegruchy Jan 20, 2025
335fb9c
Remove restriction on multiple RequestDetails for non-embedded parame…
lukedegruchy Jan 20, 2025
53810b3
More refactoring baby steps.
lukedegruchy Jan 21, 2025
799a007
Even more refactoring baby steps. Spotless.
lukedegruchy Jan 21, 2025
a139c33
Refactor functionality into a separate class.
lukedegruchy Jan 21, 2025
dab0dc1
Spotless.
lukedegruchy Jan 21, 2025
169a10d
Resolve many TODOs. More refactoring and renaming. Javadoc. Spotless.
lukedegruchy Jan 21, 2025
7e2ce77
Resolve many TODOs. More refactoring and renaming. Better testing. …
lukedegruchy Jan 22, 2025
4dc6552
TODOs. Exception error messages. Start trying to support ZonedDateT…
lukedegruchy Jan 22, 2025
0f0837f
Barely implement ZonedDateTime conversion.
lukedegruchy Jan 23, 2025
35a5318
Copyright headers. Address TODOs. Convert the other params classes …
lukedegruchy Jan 23, 2025
73b5a1f
Spotless.
lukedegruchy Jan 23, 2025
dc06188
Renames refactors cleanup TODOs.
lukedegruchy Jan 23, 2025
2150f1c
Merge remote-tracking branch 'origin/master' into ld-20250110-feature…
lukedegruchy Jan 23, 2025
69e1411
Integrate with clinical-reasoning master. Introduce new annotations…
lukedegruchy Jan 23, 2025
365b746
Rename classes. Add annotations to caregaps and multimeasures. Star…
lukedegruchy Jan 23, 2025
33d818f
Spotless.
lukedegruchy Jan 23, 2025
ac9451b
Commit non-breaking changes to migrate towards the new annotations.
lukedegruchy Jan 24, 2025
c42da79
Spotless. Logging.
lukedegruchy Jan 24, 2025
92d40ae
Fix new logic for embedded params. No cleanup yet.
lukedegruchy Jan 24, 2025
3161bb5
Move closer to remove the old embedded params functionality. Totally…
lukedegruchy Jan 24, 2025
a685744
Delete EmbeddedOperationParam.java and EmbeddedOperationParameter.jav…
lukedegruchy Jan 24, 2025
f299cfa
Carve out and delete all extraneous code from MethodUtil to EmbeddedP…
lukedegruchy Jan 24, 2025
b6977df
Cleanup. javadoc.
lukedegruchy Jan 26, 2025
5231588
Spotless.
lukedegruchy Jan 26, 2025
3c13065
Spotless. Partially refactor MethodUtil. Fix recently introduced bug.
lukedegruchy Jan 26, 2025
494f1d0
Spotless.
lukedegruchy Jan 26, 2025
b44ee9d
Simplify implementation even more. Support a separate method to do n…
lukedegruchy Jan 26, 2025
ba41e49
Fix error message and assertion.
lukedegruchy Jan 26, 2025
b823a1e
Spotless.
lukedegruchy Jan 26, 2025
507850a
Remove logging to address PHI errors.
lukedegruchy Jan 27, 2025
c548b2c
Fix tests.
lukedegruchy Jan 27, 2025
2fba720
Rename common method reflection test class. Add all MethodUtilTest m…
lukedegruchy Jan 28, 2025
da05e9d
Spotless. Convert types in parmas constructors. Fix Builder constru…
lukedegruchy Jan 29, 2025
4d000c4
Fix compile and startup errors. Relax validation.
lukedegruchy Jan 30, 2025
2184de5
Spotless
lukedegruchy Jan 30, 2025
c4b3100
Initial attempt to support the Header annotation.
lukedegruchy Jan 30, 2025
146ea0e
Spotless.
lukedegruchy Jan 30, 2025
037ffd7
Add test cases for @Header.
lukedegruchy Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2025 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%
*/
package ca.uhn.fhir.rest.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates a class will contain {@link OperationParam} parameters and similar annotations that will be processed
* in place of separate method parameters so annotated in an operation provider.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
public @interface EmbeddableOperationParams {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2025 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%
*/
package ca.uhn.fhir.rest.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates that a method parameter is for a class annotated with {@link EmbeddableOperationParams} which will in turn
* contain a constructor whose parameters will be annotated with {@link OperationParam} and similar annotations.
* a
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PARAMETER})
public @interface EmbeddedOperationParams {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ca.uhn.fhir.rest.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// LUKETODO: javadoc
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface Header {
String value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface IdParam {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@
*/
Class<? extends IBase> type() default IBase.class;

/**
* The source type of the parameters if we're expecting to do a type conversion, such as String to ZonedDateTime.
* Void indicates that we don't want to do a type conversion.
*
* @return the source type of the parameter
*/
Class<?> sourceType() default Void.class;

/**
* @return The range type associated with any type conversion. For instance, if we expect a start and end date.
* NOT_APPLICABLE is the default and indicates range conversion is not applicable.
*/
OperationParameterRangeType rangeType() default OperationParameterRangeType.NOT_APPLICABLE;

/**
* Optionally specifies the type of the parameter as a string, such as <code>Coding</code> or
* <code>base64Binary</code>. This can be useful if you want to use a generic interface type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2025 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%
*/
package ca.uhn.fhir.rest.annotation;

/**
* Used to indicate whether an {@link OperationParam} should be considered as part of a range of values, and if
* so whether it's the start or end of the range.
*/
public enum OperationParameterRangeType {
START,
END,
NOT_APPLICABLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlUtil;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

Expand Down Expand Up @@ -120,21 +121,22 @@ public static Integer findIdParameterIndex(Method theMethod, FhirContext theCont
if (IIdType.class.equals(paramType)) {
return index;
}
boolean isRi = theContext.getVersion().getVersion().isRi();
boolean usesHapiId = IdDt.class.equals(paramType);
if (isRi == usesHapiId) {
throw new ConfigurationException(Msg.code(1936)
+ "Method uses the wrong Id datatype (IdDt / IdType) for the given context FHIR version: "
+ theMethod.toString());
}
validateIdType(theMethod, theContext, paramType);
}
return index;
}

// public static Integer findSinceParameterIndex(Method theMethod) {
// return findParamIndex(theMethod, Since.class);
// }
public static void validateIdType(Method theMethod, FhirContext theContext, Class<?> paramType) {
boolean isRi = theContext.getVersion().getVersion().isRi();
boolean usesHapiId = IdDt.class.equals(paramType);
if (isRi == usesHapiId) {
throw new ConfigurationException(Msg.code(1936)
+ "Method uses the wrong Id datatype (IdDt / IdType) for the given context FHIR version: "
+ theMethod.toString());
}
}

@Nullable
public static Integer findParamAnnotationIndex(Method theMethod, Class<?> toFind) {
int paramIndex = 0;
for (Annotation[] annotations : theMethod.getParameterAnnotations()) {
Expand All @@ -149,6 +151,7 @@ public static Integer findParamAnnotationIndex(Method theMethod, Class<?> toFind
return null;
}

@Nullable
public static Object fromInteger(Class<?> theType, IntegerDt theArgument) {
if (theArgument == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,4 +636,8 @@ public static List<String> extractExamples(Annotation[] theParameterAnnotations)
}
return retVal;
}

public static boolean isOneOfEligibleTypes(Class<?> theTypeToCheck, Class<?>... theEligibleTypes) {
return Arrays.stream(theEligibleTypes).anyMatch(eligibleType -> eligibleType == theTypeToCheck);
}
}
44 changes: 44 additions & 0 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.Validate;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
Expand All @@ -40,6 +42,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class ReflectionUtil {

Expand Down Expand Up @@ -165,6 +168,28 @@ public static Class<?> getGenericCollectionTypeOfMethodParameter(Method theMetho
return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);
}

public static Class<?> getGenericCollectionTypeOfConstructorParameter(
Constructor<?> theConstructor, Parameter theConstructorParameter) {
final int theParamIndex = getIndexOfElement(theConstructor.getParameters(), theConstructorParameter);
final Type genericParameterType = theConstructor.getGenericParameterTypes()[theParamIndex];

if (Class.class.equals(genericParameterType) || Class.class.equals(genericParameterType.getClass())) {
return null;
}

final ParameterizedType collectionType = (ParameterizedType) genericParameterType;
return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);
}

private static <T> int getIndexOfElement(T[] array, T element) {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(element)) {
return i;
}
}
return -1; // Return -1 if the element is not found
}

public static Class<?> getGenericCollectionTypeOfMethodReturnType(Method theMethod) {
Type genericReturnType = theMethod.getGenericReturnType();
if (!(genericReturnType instanceof ParameterizedType)) {
Expand Down Expand Up @@ -290,4 +315,23 @@ public static boolean typeExists(String theName) {
return false;
}
}

public static List<Class<?>> getMethodParamsWithClassesWithFieldsWithAnnotation(
Method theMethod, Class<? extends Annotation> theAnnotationClass) {
return Arrays.stream(theMethod.getParameterTypes())
.filter(paramType -> hasAnyFieldsWithAnnotation(paramType, theAnnotationClass))
.collect(Collectors.toList());
}

public static boolean hasAnyMethodParamsWithClassesWithFieldsWithAnnotation(
Method theMethod, Class<? extends Annotation> theAnnotationClass) {
return Arrays.stream(theMethod.getParameterTypes())
.anyMatch(paramType -> hasAnyFieldsWithAnnotation(paramType, theAnnotationClass));
}

private static boolean hasAnyFieldsWithAnnotation(
Class<?> paramType, Class<? extends Annotation> theAnnotationClass) {
return Arrays.stream(paramType.getDeclaredFields())
.anyMatch(field -> field.isAnnotationPresent(theAnnotationClass));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import ca.uhn.fhir.i18n.Msg;
import org.junit.jupiter.api.Test;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -65,4 +67,39 @@ public void testDescribeMethod() throws NoSuchMethodException {
assertEquals("startsWith returns(boolean) params(java.lang.String, int)", description);
}

@Retention(RetentionPolicy.RUNTIME)
@interface TestAnnotation {}

static class TestClass1 {
@TestAnnotation
private String field1;
}

static class TestClass2 {
private String field2;
}

static class TestClass3 {
@TestAnnotation
private String field3;
}

static class TestClass4 {
private TestClass1 param1;
private TestClass2 param2;
private TestClass3 param3;

void setParams(TestClass1 param1, TestClass2 param2, TestClass3 param3) {
}
}

@Test
public void testGetMethodParamsWithClassesWithFieldsWithAnnotation() throws NoSuchMethodException {
Method method = TestClass4.class.getDeclaredMethod("setParams", TestClass1.class, TestClass2.class, TestClass3.class);
List<Class<?>> result = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation(method, TestAnnotation.class);

assertEquals(2, result.size());
assertTrue(result.contains(TestClass1.class));
assertTrue(result.contains(TestClass3.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.fail;


public class ResourceProviderDstu2ValueSetTest extends BaseResourceProviderDstu2Test {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,19 @@ private void findResourceMethods(Object theProvider) {
Class<?> clazz = theProvider.getClass();
Class<?> supertype = clazz.getSuperclass();
while (!Object.class.equals(supertype)) {
// ourLog.info("1234: findResourceMethods: findResourceMethodsOnInterfaces for provider class: {}",
// theProvider.getClass());
count += findResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces());
// ourLog.info("1234: findResourceMethods: findResourceMethods for provider class: {}",
// theProvider.getClass());
count += findResourceMethods(theProvider, supertype);
supertype = supertype.getSuperclass();
}

try {
// ourLog.info("1234: findResourceMethodsOnInterfaces for provider class: {}", theProvider.getClass());
count += findResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces());
// ourLog.info("1234: findResourceMethods for provider class: {}", theProvider.getClass());
count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) {
throw new ConfigurationException(
Expand All @@ -472,7 +478,13 @@ private void findResourceMethods(Object theProvider) {
private int findResourceMethodsOnInterfaces(Object theProvider, Class<?>[] interfaces) {
int count = 0;
for (Class<?> anInterface : interfaces) {
// final List<Class<?>> innerInterfaces = Arrays.stream(anInterface.getInterfaces()).map(innerinterface ->
// innerinterface.getClass()).collect(Collectors.toUnmodifiableList());
// ourLog.info("1234: findResourceMethodsOnInterfaces for provider class: {} and interface: {}",
// theProvider.getClass(), innerInterfaces);
count += findResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces());
// ourLog.info("1234: findResourceMethodsOnInterfaces for provider class: {} and interface: {}",
// theProvider.getClass(), anInterface.getClass());
count += findResourceMethods(theProvider, anInterface);
}
return count;
Expand Down
Loading
Loading