From 8537709222759bb75c6325981d2d60b5eba9cf08 Mon Sep 17 00:00:00 2001 From: Jonathan Percival Date: Wed, 23 Oct 2024 11:15:58 -0600 Subject: [PATCH 01/75] WIP --- .../fhir/rest/annotation/OperationParam.java | 2 +- .../rest/server/method/BaseMethodBinding.java | 15 +---- .../BaseOutcomeReturningMethodBinding.java | 2 +- .../server/method/OperationMethodBinding.java | 2 + .../server/method/OperationParameter.java | 18 ++--- .../r4/measure/MeasureOperationsProvider.java | 67 ++++++++++++++++++- 6 files changed, 82 insertions(+), 24 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index 2a66ed00daa5..4619878246ac 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -31,7 +31,7 @@ /** */ @Retention(RetentionPolicy.RUNTIME) -@Target(value = ElementType.PARAMETER) +@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) public @interface OperationParam { /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 94095d8bfa0c..1dfbb5dd5a43 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -110,6 +110,7 @@ protected List getQueryParameters() { return myQueryParameters; } + // JP TODO: Alternatively, do the mapping here? protected Object[] createMethodParams(RequestDetails theRequest) { Object[] params = new Object[getParameters().size()]; for (int i = 0; i < getParameters().size(); i++) { @@ -121,18 +122,6 @@ protected Object[] createMethodParams(RequestDetails theRequest) { return params; } - protected Object[] createParametersForServerRequest(RequestDetails theRequest) { - Object[] params = new Object[getParameters().size()]; - for (int i = 0; i < getParameters().size(); i++) { - IParameter param = getParameters().get(i); - if (param == null) { - continue; - } - params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); - } - return params; - } - /** * Subclasses may override to declare that they apply to all resource types */ @@ -260,6 +249,8 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // Actually invoke the method try { + // JP - TODO: check to see if we have a single + // class, bind to the fields, then invoke. Method method = getMethod(); return method.invoke(getProvider(), theMethodParams); } catch (InvocationTargetException e) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBinding.java index d7eae75e21c0..ffda59f989c0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBinding.java @@ -163,7 +163,7 @@ public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequ public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { - Object[] params = createParametersForServerRequest(theRequest); + Object[] params = createMethodParams(theRequest); addParametersForServerRequest(theRequest, params); /* diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 4b312b38efbb..b2ecb3c945bb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -184,6 +184,8 @@ protected OperationMethodBinding( } else { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; myCanOperateAtInstanceLevel = true; + + // JP TODO: Here, we need to check the operation's parameters class for the Id. for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { if (next instanceof IdParam) { myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 149f5b839b7b..923c88ddf30f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -85,12 +85,12 @@ public class OperationParameter implements IParameter { private Class myInnerCollectionType; private int myMax; - private int myMin; + private final int myMin; private Class myParameterType; private String myParamType; private SearchParameter mySearchParameterBinding; - private String myDescription; - private List myExampleValues; + private final String myDescription; + private final List myExampleValues; OperationParameter( FhirContext theCtx, @@ -117,7 +117,7 @@ public class OperationParameter implements IParameter { @SuppressWarnings({"rawtypes", "unchecked"}) private void addValueToList(List matchingParamValues, Object values) { if (values != null) { - if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { + if (BaseAndListParam.class.isAssignableFrom(myParameterType) && !matchingParamValues.isEmpty()) { BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); BaseAndListParam newAndList = (BaseAndListParam) values; for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { @@ -258,7 +258,7 @@ public static void validateTypeIsAppropriateVersionForContext( } } - public OperationParameter setConverter(IOperationParamConverter theConverter) { + OperationParameter setConverter(IOperationParamConverter theConverter) { myConverter = theConverter; return this; } @@ -302,7 +302,7 @@ private void translateQueryParametersIntoServerArgumentForGet( RequestDetails theRequest, List matchingParamValues) { if (mySearchParameterBinding != null) { - List params = new ArrayList(); + List params = new ArrayList<>(); String nameWithQualifierColon = myName + ":"; for (String nextParamName : theRequest.getParameters().keySet()) { @@ -438,7 +438,7 @@ private void translateQueryParametersIntoServerArgumentForPost( List values = paramChildAccessor.getValues(requestContents); for (IBase nextParameter : values) { List nextNames = nameChild.getAccessor().getValues(nextParameter); - if (nextNames != null && nextNames.size() > 0) { + if (nextNames != null && !nextNames.isEmpty()) { IPrimitiveType nextName = (IPrimitiveType) nextNames.get(0); if (myName.equals(nextName.getValueAsString())) { @@ -449,9 +449,9 @@ private void translateQueryParametersIntoServerArgumentForPost( valueChild.getAccessor().getValues(nextParameter); List paramResources = resourceChild.getAccessor().getValues(nextParameter); - if (paramValues != null && paramValues.size() > 0) { + if (paramValues != null && !paramValues.isEmpty()) { tryToAddValues(paramValues, matchingParamValues); - } else if (paramResources != null && paramResources.size() > 0) { + } else if (paramResources != null && !paramResources.isEmpty()) { tryToAddValues(paramResources, matchingParamValues); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index e9b8d6bab263..eca8a10a1a84 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import org.hl7.fhir.OperationOutcome; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; @@ -34,6 +35,7 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.utility.monad.Eithers; public class MeasureOperationsProvider { @@ -78,7 +80,7 @@ public MeasureReport evaluateMeasure( @OperationParam(name = "reportType") String theReportType, @OperationParam(name = "subject") String theSubject, @OperationParam(name = "practitioner") String thePractitioner, - @OperationParam(name = "lastReceivedOn") String theLastReceivedOn, + @OperationPar:am(name = "lastReceivedOn") String theLastReceivedOn, @OperationParam(name = "productLine") String theProductLine, @OperationParam(name = "additionalData") Bundle theAdditionalData, @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, @@ -102,4 +104,67 @@ public MeasureReport evaluateMeasure( theProductLine, thePractitioner); } + + /** + * { + * "resourceType": "OperationDefinition", + * "name": "fooBar", + * "url": "http://foo.bar", + * "parameters": [ + * { + * "name": "doFoo", + * "type: "boolean", + * "use": "in" + * }, + * { + * "name": "count", + * "type: "integer", + * "use": "in" + * }, + * { + * "name": "return", + * "use": "out", + * "type": "OperationOutcome" + * } + * ] + * } + **/ + + + @Operation(name = "fooBar") + OperationOutcome fooBar(FooBarParams theParams) { + return new OperationOutcome(); + } + + + + + void example() { + fooBar(new FooBarParams()); + } + + @OperationParam + class FooBarParams { + + @OperationParam(name = "doFoo") + private Boolean myDoFoo; + @OperationParam(name = "quanity") + private Quantity quantity; + + public Boolean getDoFoo() { + return myDoFoo; + } + + public void setDoFoo(Boolean theDoFoo) { + myDoFoo = theDoFoo; + } + + public Integer getCount() { + return myCount; + } + + public void setCount(Integer theCount) { + myCount = theCount; + } + } } From b5094de8a142440ed58e8eb883345308b0f26e12 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 13 Jan 2025 16:37:09 -0500 Subject: [PATCH 02/75] cdr boots but there's a runtime error when calling the rest method: wrong number of parameters. --- .../annotation/OperationEmbeddedType.java | 22 +++ .../uhn/fhir/rest/server/RestfulServer.java | 12 ++ .../rest/server/method/BaseMethodBinding.java | 6 +- .../fhir/rest/server/method/MethodUtil.java | 166 +++++++++++++++++- .../server/method/OperationMethodBinding.java | 8 +- .../server/method/OperationParameter.java | 6 + .../uhn/fhir/cr/r4/measure/FooBarParams.java | 69 ++++++++ .../r4/measure/MeasureOperationsProvider.java | 78 ++------ .../dstu2/ServerConformanceProvider.java | 1 + .../ServerCapabilityStatementProvider.java | 1 + 10 files changed, 299 insertions(+), 70 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java create mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java new file mode 100644 index 000000000000..057e18b22ee2 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java @@ -0,0 +1,22 @@ +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: better name? +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE}) +public @interface OperationEmbeddedType { + /** + * The name of the embedded type + */ + // LUKETODO: default ""??? + String name() default ""; + + // LUKETODO: javadoc: name of the type + // LUKETODO: extends and defaults what???? + // Class type() default IBase.class; + Class type() default Object.class; +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 9724bdc50464..d5d16979db79 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -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( @@ -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> 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; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 663bb36236bf..627da6095802 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -110,8 +110,9 @@ protected List getQueryParameters() { return myQueryParameters; } - // JP TODO: Alternatively, do the mapping here? + // LUKETODO: Alternatively, do the mapping here? protected Object[] createMethodParams(RequestDetails theRequest) { + ourLog.info("1234: Creating parameters for method {}, and requestDetails: {}", myMethod.getName(), theRequest); Object[] params = new Object[getParameters().size()]; for (int i = 0; i < getParameters().size(); i++) { IParameter param = getParameters().get(i); @@ -249,9 +250,10 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // Actually invoke the method try { - // JP - TODO: check to see if we have a single + // LUKETODO: check to see if we have a single // class, bind to the fields, then invoke. Method method = getMethod(); + ourLog.info("1234: invoking method for: {} and params: {}", method.getName(), theMethodParams); return method.invoke(getProvider(), theMethodParams); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index e3c12c79caa0..4c61fbe9e995 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -37,6 +37,7 @@ import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Patch; @@ -68,16 +69,22 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class MethodUtil { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class); + /** * Non instantiable */ @@ -102,6 +109,118 @@ public static List getResourceParameters( Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; + // LUKETODO: one param per method parameter: what happens if we expand this? + + if (Arrays.stream(parameterTypes) + .anyMatch(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class))) { + ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); + + // This is the @Operation parameter on the method itself (ex: evaluateMeasure) + final Operation op = theMethod.getAnnotation(Operation.class); + + if (parameterTypes.length > 1) { + // LUKETODO: error + throw new ConfigurationException( + Msg.code(99999) + "Only one OperationEmbeddedType is supported for now!"); + } + + final Class operationEmbeddedType = parameterTypes[0]; + + final Field[] fields = operationEmbeddedType.getDeclaredFields(); + ourLog.info("1234: declaredFields: {}", fields.length); + for (Field field : fields) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final Annotation[] fieldAnnotations = field.getAnnotations(); + + final Set annotationClassNames = Arrays.stream(fieldAnnotations) + .map(Annotation::annotationType) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); + + ourLog.info( + "1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, fieldAnnotations: {}", + fieldName, + fieldType.getName(), + annotationClassNames); + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(Msg.code(99999) + "More than one annotation per field!"); + } + + // This is the parameter on the field in question on the OperationEmbeddedType class: ex myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + final IParameter param; + + // LUKETODO: what if this is not a IdParam or an OperationParam? + if (fieldAnnotation instanceof IdParam) { + param = new NullParameter(); + } else if (fieldAnnotation instanceof OperationParam) { + final OperationParam operationParam = (OperationParam) fieldAnnotation; + + final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + final OperationParameter operationParameter = new OperationParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + + // Not sure what these are, but I think they're for params that are part of a Collection parameter + // and may have soemthing to do with a SearchParameter + final Class> outerCollectionType = null; + final Class> innerCollectionType = null; + + // LUKETODO: how is this thing supposed to work????? + // final String paramTypeName = operationParam.typeName(); + final String paramTypeName = operationParam.name(); + + operationParameter.initializeTypes(theMethod, outerCollectionType, innerCollectionType, fieldType); + + param = operationParameter; + } else { + throw new ConfigurationException(Msg.code(99999) + "Unsupport param fieldType: " + fieldAnnotation); + } + + // LUKETODO: somehow add multiples of parameters in this block + + // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType + // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType + + // // This is a 2x array that contains fieldAnnotations across all methods and each method can have + // multiple fieldAnnotations + // final Annotation[][] parameterAnnotations = theMethod.getParameterAnnotations(); + // + // // This corresponds to a single @OperationParam, like a single parameter in evaluateMeasure + // // in OUR case, we want this to correspond to a single FIELD in our @OperationEmbeddedType + // final OperationParam operationParam = (OperationParam) fieldAnnotations[0]; + // + // final String description = ParametersUtil.extractDescription(nextParameterAnnotations); + // final List examples = ParametersUtil.extractExamples(nextParameterAnnotations); + // + // final IParameter param = param = new OperationParameter( + // theContext, + // op.name(), + // operationParam.name(), + // operationParam.min(), + // operationParam.max(), + // description, + // examples); + + parameters.add(param); + } + + // LUKETODO: short-circuit for now + return parameters; + } + for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { IParameter param = null; @@ -113,7 +232,38 @@ public static List getResourceParameters( // TagList is handled directly within the method bindings param = new NullParameter(); } else { - if (Collection.class.isAssignableFrom(parameterType)) { + // LUKETODO: add comments about what this does + if (parameterType.isAnnotationPresent(OperationEmbeddedType.class)) { + // ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); + // + // final Field[] fields = parameterType.getDeclaredFields(); + // ourLog.info("1234: declaredFields: {}", fields.length); + // ourLog.info("1234: MethodUtil: nextParameterAnnotations: {}", + // Arrays.toString(nextParameterAnnotations)); + // for (Field field : fields) { + // final String fieldName = field.getName(); + // final Class type = field.getType(); + // final Annotation[] annotations = field.getAnnotations(); + // + // final Set annotationClassNames = + // Arrays.stream(annotations).map(Annotation::annotationType).map(Class::getName).collect(Collectors.toUnmodifiableSet()); + // + // ourLog.info("1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, + // annotations: {}", fieldName, type.getName(), annotationClassNames); + // + // // LUKETODO: somehow add multiples of parameters in this block + // + // // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType + // // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType + // } + // + // // LUKETODO: figure out the pattern here and possibly reuse it: + // // LUKETODO: take the original parameterType and see if it's a Collection + // // LUKETODO: take the generic parameter type for the Collection, and assign it to + // parameterType + // // LUKETODO: as a sort of guard, if the parametter if null, then get the superclass for the + // method, get the superclass method, and then get the generic type for the superclass method??? + } else if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { @@ -132,12 +282,16 @@ public static List getResourceParameters( } declaredParameterType = parameterType; } + // LUKETODO: now we're processing the generic parameter, so capture the inner and outer types + // Collection + // LUKETODO: using reflection, find the if (Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); declaredParameterType = parameterType; } + // LUKETODO: as a guard: if this is still a Collection, then throw because something went wrong if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName() @@ -304,7 +458,7 @@ public static List getResourceParameters( OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - ; + param = new OperationParameter( theContext, op.name(), @@ -332,7 +486,7 @@ public static List getResourceParameters( parameterType = newParameterType; } } else if (nextAnnotation instanceof Validate.Mode) { - if (parameterType.equals(ValidationModeEnum.class) == false) { + if (!parameterType.equals(ValidationModeEnum.class)) { throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName()); @@ -368,7 +522,7 @@ public Object outgoingClient(Object theObject) { } }); } else if (nextAnnotation instanceof Validate.Profile) { - if (parameterType.equals(String.class) == false) { + if (!parameterType.equals(String.class)) { throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); @@ -408,6 +562,10 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } + // LUKETODO: if we call this with an @OperationEmbeddedType, we get an Exceptioon here + // LUKETODO: Or do we expand the paramters here, and then foreaach parameters.add() ??? + // ourLog.info("1234: param class: {}, method: {}", param.getClass().getCanonicalName(), + // theMethod.getName()); param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); parameters.add(param); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 1be3a5ce914e..6ee9d495a228 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -58,6 +58,8 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class); + public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; private final boolean myIdempotent; private final boolean myDeleteEnabled; @@ -185,10 +187,12 @@ protected OperationMethodBinding( myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; myCanOperateAtInstanceLevel = true; - // JP TODO: Here, we need to check the operation's parameters class for the Id. + // LUKETODO: Here, we need to check the operation's parameters class for the Id. for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { + // ourLog.info("1234: method: {}, param: {}, paramType: {}", theMethod.getName(), myIdParamIndex, + // next.annotationType()); if (next instanceof IdParam) { - myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; + myCanOperateAtTypeLevel = ((IdParam) next).optional(); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 0ccbf32faf9b..464f6f52e748 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -70,6 +71,8 @@ public class OperationParameter implements IParameter { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationParameter.class); + static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") @@ -200,11 +203,14 @@ public void initializeTypes( || isSearchParam || ValidationModeEnum.class.equals(myParameterType); + final boolean isAnnotationPresent = myParameterType.isAnnotationPresent(OperationEmbeddedType.class); + /* * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We * should probably clean this up.. */ if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { + // LUKETODO: this is where we get the Exception: add an else if if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java new file mode 100644 index 000000000000..5e414877994c --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java @@ -0,0 +1,69 @@ +package ca.uhn.fhir.cr.r4.measure; + +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IntegerType; + +import java.util.StringJoiner; + +// LUKETODO: new annotation +// LUKETODO: look at embedded annotations in JPA and follow that pattern +/** + * { + * "resourceType": "OperationDefinition", + * "name": "fooBar", + * "url": "http://foo.bar", + * "parameters": [ + * { + * "name": "doFoo", + * "type: "boolean", + * "use": "in" + * }, + * { + * "name": "count", + * "type: "integer", + * "use": "in" + * }, + * { + * "name": "return", + * "use": "out", + * "type": "OperationOutcome" + * } + * ] + * } + **/ +@OperationEmbeddedType +public class FooBarParams { + @OperationParam(name = "doFoo") + // LUKETODO: do I always need to make it a BooleanType and not a Boolean? + private BooleanType myDoFoo; + + @OperationParam(name = "count") + private IntegerType myCount; + + public BooleanType getDoFoo() { + return myDoFoo; + } + + public void setDoFoo(BooleanType theDoFoo) { + myDoFoo = theDoFoo; + } + + // LUKETODO: do I always need to make it a IntegerType and not an Integer? + public IntegerType getCount() { + return myCount; + } + + public void setCount(IntegerType theCount) { + myCount = theCount; + } + + @Override + public String toString() { + return new StringJoiner(", ", FooBarParams.class.getSimpleName() + "[", "]") + .add("myDoFoo=" + myDoFoo) + .add("myCount=" + myCount) + .toString(); + } +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 65a8b153e86a..3a8b4bea7687 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import org.hl7.fhir.OperationOutcome; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; @@ -35,10 +34,12 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class MeasureOperationsProvider { + private static final Logger ourLog = LoggerFactory.getLogger(MeasureOperationsProvider.class); private final R4MeasureEvaluatorSingleFactory myR4MeasureServiceFactory; private final StringTimePeriodHandler myStringTimePeriodHandler; @@ -81,13 +82,14 @@ public MeasureReport evaluateMeasure( @OperationParam(name = "reportType") String theReportType, @OperationParam(name = "subject") String theSubject, @OperationParam(name = "practitioner") String thePractitioner, - @OperationPar:am(name = "lastReceivedOn") String theLastReceivedOn, + @OperationParam(name = "lastReceivedOn") String theLastReceivedOn, @OperationParam(name = "productLine") String theProductLine, @OperationParam(name = "additionalData") Bundle theAdditionalData, @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, @OperationParam(name = "parameters") Parameters theParameters, RequestDetails theRequestDetails) throws InternalErrorException, FHIRException { + // LUKETODO: Parameters within Parameters return myR4MeasureServiceFactory .create(theRequestDetails) .evaluate( @@ -106,66 +108,18 @@ public MeasureReport evaluateMeasure( thePractitioner); } - /** - * { - * "resourceType": "OperationDefinition", - * "name": "fooBar", - * "url": "http://foo.bar", - * "parameters": [ - * { - * "name": "doFoo", - * "type: "boolean", - * "use": "in" - * }, - * { - * "name": "count", - * "type: "integer", - * "use": "in" - * }, - * { - * "name": "return", - * "use": "out", - * "type": "OperationOutcome" - * } - * ] - * } - **/ - + // @Operation(name = "$fooBar", manualResponse = true, idempotent = true) + // OperationOutcome fooBar(FooBarParams theParams) { + // ourLog.info("fooBar params: {}", theParams); + // return new OperationOutcome(); + // } - @Operation(name = "fooBar") - OperationOutcome fooBar(FooBarParams theParams) { - return new OperationOutcome(); + @Operation(name = "$fooBar", manualResponse = true, idempotent = true) + public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { + ourLog.info("fooBar params: {}", theParams); } - - - - void example() { - fooBar(new FooBarParams()); - } - - @OperationParam - class FooBarParams { - - @OperationParam(name = "doFoo") - private Boolean myDoFoo; - @OperationParam(name = "quanity") - private Quantity quantity; - - public Boolean getDoFoo() { - return myDoFoo; - } - - public void setDoFoo(Boolean theDoFoo) { - myDoFoo = theDoFoo; - } - - public Integer getCount() { - return myCount; - } - - public void setCount(Integer theCount) { - myCount = theCount; - } - } + void example() { + fooBar(new FooBarParams()); + } } diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index 572d7c2ed9a2..553c4c97d90e 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -506,6 +506,7 @@ public OperationDefinition readOperationDefinition(@IdParam IdDt theId, RequestD if (!inParams.add(nextParam.getName())) { continue; } + // LUKETODO: how to handle multiple parameters? Parameter param = op.addParameter(); param.setUse(OperationParameterUseEnum.IN); if (nextParam.getParamType() != null) { diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java index ca200ea79827..2eeab81955f1 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java @@ -638,6 +638,7 @@ private OperationDefinition readOperationDefinitionForOperation(List Date: Tue, 14 Jan 2025 10:08:46 -0500 Subject: [PATCH 03/75] Simple void method with OperationEmbeddedType works. --- .../rest/server/method/BaseMethodBinding.java | 62 ++++++++++++++++++- .../fhir/rest/server/method/MethodUtil.java | 1 + .../server/method/BaseMethodBindingTest.java | 7 +++ .../rest/server/method/MethodUtilTest.java | 7 +++ .../uhn/fhir/cr/r4/measure/FooBarParams.java | 9 ++- .../r4/measure/MeasureOperationsProvider.java | 12 +++- .../cr/r4/measure/ReturnsOutcomeParams.java | 41 ++++++++++++ 7 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingTest.java create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java create mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 627da6095802..a3fec5cbd9eb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.Patch; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Search; @@ -56,9 +57,13 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -252,8 +257,61 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th try { // LUKETODO: check to see if we have a single // class, bind to the fields, then invoke. - Method method = getMethod(); - ourLog.info("1234: invoking method for: {} and params: {}", method.getName(), theMethodParams); + final Method method = getMethod(); + + // LUKETODO: split this up into private methods + final Class[] parameterTypes = method.getParameterTypes(); + + ourLog.info("1234: invoking method for: {} and params: {} and parameterTypes: {}", method.getName(), theMethodParams, Arrays.toString(parameterTypes)); + + for (Class parameterType : parameterTypes) { + ourLog.info("1234: invoking parameterType: {} and method: {}", parameterType, method.getName()); + final Annotation[] parameterTypeAnnotations = parameterType.getAnnotations(); + + final boolean hasOperationEmbeddedTypeAnnotation = + Arrays.stream(parameterTypeAnnotations) + .anyMatch(OperationEmbeddedType.class::isInstance); + + if (hasOperationEmbeddedTypeAnnotation) { + final Constructor[] constructors = parameterType.getConstructors(); + + // LUKETODO: what if there's a noarg constructor and all we have is setters? + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + if (constructors.length > 0) { + // LUKETODO: if there are multiple constructors, cycle through them until you find one that matches the params list + final Constructor constructor = constructors[0]; + + final Parameter[] parameters = constructor.getParameters(); + + if (parameters.length > 0) { + // LUKETODO: call setters + } + + // LUKETODO: else? + if (theMethodParams.length != parameters.length) { + throw new RuntimeException("1234: bad params"); + } + + for (int index = 0; index < theMethodParams.length; index++) { + final Class methodParamAtIndex = theMethodParams[index].getClass(); + final Class parameterAtIndex = parameters[index].getType(); + + ourLog.info("1234: methodParamAtIndex: {}, parameterAtIndex: {}", methodParamAtIndex, parameterAtIndex); + if (methodParamAtIndex != parameterAtIndex) { + throw new RuntimeException("1234: bad params"); + } + } + + final Object operationEmbeddedType = constructor.newInstance(theMethodParams); + + ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); + return method.invoke(getProvider(), operationEmbeddedType); + } + } + } + + // LUKETODO: here we fail with: java.lang.IllegalArgumentException: wrong number of arguments return method.invoke(getProvider(), theMethodParams); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4c61fbe9e995..4a33bd6ee91e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -111,6 +111,7 @@ public static List getResourceParameters( int paramIndex = 0; // LUKETODO: one param per method parameter: what happens if we expand this? + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! if (Arrays.stream(parameterTypes) .anyMatch(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class))) { ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingTest.java new file mode 100644 index 000000000000..84b37a278faf --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingTest.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.rest.server.method; + +import static org.junit.jupiter.api.Assertions.*; + +class BaseMethodBindingTest { + +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java new file mode 100644 index 000000000000..8028f0fe1479 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.rest.server.method; + +import static org.junit.jupiter.api.Assertions.*; + +class MethodUtilTest { + +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java index 5e414877994c..1833bbf05f4f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java @@ -42,6 +42,11 @@ public class FooBarParams { @OperationParam(name = "count") private IntegerType myCount; + public FooBarParams(BooleanType myDoFoo, IntegerType myCount) { + this.myDoFoo = myDoFoo; + this.myCount = myCount; + } + public BooleanType getDoFoo() { return myDoFoo; } @@ -62,8 +67,8 @@ public void setCount(IntegerType theCount) { @Override public String toString() { return new StringJoiner(", ", FooBarParams.class.getSimpleName() + "[", "]") - .add("myDoFoo=" + myDoFoo) - .add("myCount=" + myCount) + .add("myDoFoo=" + myDoFoo.getValue()) + .add("myCount=" + myCount.asStringValue()) .toString(); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 3a8b4bea7687..22d0704dc5c9 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import org.hl7.fhir.OperationOutcome; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; @@ -116,10 +117,17 @@ public MeasureReport evaluateMeasure( @Operation(name = "$fooBar", manualResponse = true, idempotent = true) public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { - ourLog.info("fooBar params: {}", theParams); + ourLog.info("1234: fooBar params: {}", theParams); } +// @Operation(name = "$returnsOutcome", manualResponse = true, idempotent = true) +// public OperationOutcome returnsOutcome(@OperationParam(name = "params") ReturnsOutcomeParams theParams) { +// ourLog.info("1234: returnsOutcome params: {}", theParams); +// +// return new OperationOutcome(); +// } + void example() { - fooBar(new FooBarParams()); + fooBar(new FooBarParams(null, null)); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java new file mode 100644 index 000000000000..99a2634e45a4 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.cr.r4.measure; + +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationParam; + +import java.util.List; +import java.util.StringJoiner; + +@OperationEmbeddedType +public class ReturnsOutcomeParams { + @OperationParam(name = "oneString") + // LUKETODO: do I always need to make it a BooleanType and not a Boolean? + private String myString; + + @OperationParam(name = "multStrings") + private List myStrings; + + public String getString() { + return myString; + } + + public void setString(String myString) { + this.myString = myString; + } + + public List getStrings() { + return myStrings; + } + + public void setStrings(List myStrings) { + this.myStrings = myStrings; + } + + @Override + public String toString() { + return new StringJoiner(", ", ReturnsOutcomeParams.class.getSimpleName() + "[", "]") + .add("myString='" + myString + "'") + .add("myStrings=" + myStrings) + .toString(); + } +} From a434400fddcb950b3b667e0f776f21f6ec0eb2e1 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 14 Jan 2025 13:28:11 -0500 Subject: [PATCH 04/75] Try to support setters in new params-type classes. Successfully call methods with return types, but still can't see them in Bruno. Introduce OperationEmbeddedParameter amd use it in place of OperationParameter. Try to figure out why caregaps doesn't use same setup code as evaluateMeasure. --- .../annotation/OperationEmbeddedType.java | 1 + .../fhir/rest/annotation/OperationParam.java | 1 + .../rest/server/method/BaseMethodBinding.java | 58 +- .../fhir/rest/server/method/MethodUtil.java | 31 +- .../method/OperationEmbeddedParameter.java | 542 ++++++++++++++++++ .../server/method/OperationParameter.java | 3 + .../r4/measure/CareGapsOperationProvider.java | 4 + .../r4/measure/MeasureOperationsProvider.java | 21 +- ...meParams.java => ReturnsBundleParams.java} | 21 +- 9 files changed, 618 insertions(+), 64 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java rename hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/{ReturnsOutcomeParams.java => ReturnsBundleParams.java} (55%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java index 057e18b22ee2..951f69bb3362 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +// LUKETODO: consider not using this but instead using ONLY the replacemenet for @OperationParam // LUKETODO: better name? @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.TYPE}) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index 9fda3232c91d..49d07cc52fa8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -32,6 +32,7 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.PARAMETER, ElementType.FIELD}) +// LUKETODO: 1-1 with OperationParameter (subclass of IParameter) public @interface OperationParam { /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index a3fec5cbd9eb..30e31522700c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -282,31 +282,55 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: if there are multiple constructors, cycle through them until you find one that matches the params list final Constructor constructor = constructors[0]; - final Parameter[] parameters = constructor.getParameters(); + final Parameter[] constructorParameters = constructor.getParameters(); - if (parameters.length > 0) { + // LUKETODO: mandate an immutable class with a constructor to set params + if (constructorParameters.length == 0) { // LUKETODO: call setters - } + final Object operationEmbeddedType = constructor.newInstance(); + final List setters = Arrays.stream(parameterType.getDeclaredMethods()) + .filter(paramMethod -> paramMethod.getName().startsWith("set")) // LUKETODO: this is nasty + .collect(Collectors.toUnmodifiableList()); + + for (int index = 0; index < theMethodParams.length; index++) { + final Object methodParam = theMethodParams[index]; + final Class methodParamAtIndex = methodParam.getClass(); + // LUKETODO: check at least one param + final Method setter = setters.get(index); + final Class setterParamType = setter.getParameterTypes()[0]; + + ourLog.info("1234: methodParamAtIndex: {}, setterParamType: {}", methodParamAtIndex, setterParamType); + if (methodParamAtIndex != setterParamType) { + throw new RuntimeException("1234: bad params"); + } + + setter.invoke(methodParam); + } - // LUKETODO: else? - if (theMethodParams.length != parameters.length) { - throw new RuntimeException("1234: bad params"); - } + ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); + return method.invoke(getProvider(), operationEmbeddedType); + } else { + // LUKETODO: else? + if (theMethodParams.length != constructorParameters.length) { + throw new RuntimeException("1234: bad params"); + } - for (int index = 0; index < theMethodParams.length; index++) { - final Class methodParamAtIndex = theMethodParams[index].getClass(); - final Class parameterAtIndex = parameters[index].getType(); + for (int index = 0; index < theMethodParams.length; index++) { + final Class methodParamAtIndex = theMethodParams[index].getClass(); + final Class parameterAtIndex = constructorParameters[index].getType(); - ourLog.info("1234: methodParamAtIndex: {}, parameterAtIndex: {}", methodParamAtIndex, parameterAtIndex); - if (methodParamAtIndex != parameterAtIndex) { - throw new RuntimeException("1234: bad params"); + ourLog.info("1234: methodParamAtIndex: {}, parameterAtIndex: {}", methodParamAtIndex, parameterAtIndex); + if (methodParamAtIndex != parameterAtIndex) { + throw new RuntimeException("1234: bad params"); + } } - } - final Object operationEmbeddedType = constructor.newInstance(theMethodParams); + final Object operationEmbeddedType = constructor.newInstance(theMethodParams); + + ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); + return method.invoke(getProvider(), operationEmbeddedType); + } - ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); - return method.invoke(getProvider(), operationEmbeddedType); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4a33bd6ee91e..9dc1de60d319 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -107,6 +107,7 @@ public static List getResourceParameters( final FhirContext theContext, Method theMethod, Object theProvider) { List parameters = new ArrayList<>(); + // LUKETODO: why no caregaps here???? Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; // LUKETODO: one param per method parameter: what happens if we expand this? @@ -165,7 +166,10 @@ public static List getResourceParameters( final String description = ParametersUtil.extractDescription(fieldAnnotationArray); final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - final OperationParameter operationParameter = new OperationParameter( + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning repo +// final OperationParameter operationParameter = new OperationParameter( + final OperationEmbeddedParameter operationParameter = new OperationEmbeddedParameter( theContext, op.name(), operationParam.name(), @@ -179,10 +183,6 @@ public static List getResourceParameters( final Class> outerCollectionType = null; final Class> innerCollectionType = null; - // LUKETODO: how is this thing supposed to work????? - // final String paramTypeName = operationParam.typeName(); - final String paramTypeName = operationParam.name(); - operationParameter.initializeTypes(theMethod, outerCollectionType, innerCollectionType, fieldType); param = operationParameter; @@ -195,26 +195,6 @@ public static List getResourceParameters( // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType - // // This is a 2x array that contains fieldAnnotations across all methods and each method can have - // multiple fieldAnnotations - // final Annotation[][] parameterAnnotations = theMethod.getParameterAnnotations(); - // - // // This corresponds to a single @OperationParam, like a single parameter in evaluateMeasure - // // in OUR case, we want this to correspond to a single FIELD in our @OperationEmbeddedType - // final OperationParam operationParam = (OperationParam) fieldAnnotations[0]; - // - // final String description = ParametersUtil.extractDescription(nextParameterAnnotations); - // final List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - // - // final IParameter param = param = new OperationParameter( - // theContext, - // op.name(), - // operationParam.name(), - // operationParam.min(), - // operationParam.max(), - // description, - // examples); - parameters.add(param); } @@ -227,6 +207,7 @@ public static List getResourceParameters( IParameter param = null; Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; + Class> outerCollectionType = null; Class> innerCollectionType = null; if (TagList.class.isAssignableFrom(parameterType)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java new file mode 100644 index 000000000000..c98fac964a7d --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -0,0 +1,542 @@ +/* + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; + +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor; +import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IQueryParameterOr; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.QualifiedParamList; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.BaseAndListParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.binder.CollectionBinder; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ReflectionUtil; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Consumer; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +// LUKETODO: use this for Embedded object params +// LUKETODO: consider deleting whatever code may be unused +public class OperationEmbeddedParameter implements IParameter { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationEmbeddedParameter.class); + + static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; + + @SuppressWarnings("unchecked") + private static final Class[] COMPOSITE_TYPES = new Class[0]; + + private final FhirContext myContext; + private final String myName; + private final String myOperationName; + private boolean myAllowGet; + private IOperationParamConverter myConverter; + + @SuppressWarnings("rawtypes") + private Class myInnerCollectionType; + + private int myMax; + private final int myMin; + private Class myParameterType; + private String myParamType; + private SearchParameter mySearchParameterBinding; + private final String myDescription; + private final List myExampleValues; + + OperationEmbeddedParameter( + FhirContext theCtx, + String theOperationName, + String theParameterName, + int theMin, + int theMax, + String theDescription, + List theExampleValues) { + myOperationName = theOperationName; + myName = theParameterName; + myMin = theMin; + myMax = theMax; + myContext = theCtx; + myDescription = theDescription; + + List exampleValues = new ArrayList<>(); + if (theExampleValues != null) { + exampleValues.addAll(theExampleValues); + } + myExampleValues = Collections.unmodifiableList(exampleValues); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void addValueToList(List matchingParamValues, Object values) { + if (values != null) { + if (BaseAndListParam.class.isAssignableFrom(myParameterType) && !matchingParamValues.isEmpty()) { + BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); + BaseAndListParam newAndList = (BaseAndListParam) values; + for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { + existing.addAnd(nextAnd); + } + } else { + matchingParamValues.add(values); + } + } + } + + protected FhirContext getContext() { + return myContext; + } + + public int getMax() { + return myMax; + } + + public int getMin() { + return myMin; + } + + public String getName() { + return myName; + } + + public String getParamType() { + return myParamType; + } + + public String getSearchParamType() { + if (mySearchParameterBinding != null) { + return mySearchParameterBinding.getParamType().getCode(); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public void initializeTypes( + Method theMethod, + Class> theOuterCollectionType, + Class> theInnerCollectionType, + Class theParameterType) { + FhirContext context = getContext(); + validateTypeIsAppropriateVersionForContext(theMethod, theParameterType, context, "parameter"); + + myParameterType = theParameterType; + if (theInnerCollectionType != null) { + myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName); + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = OperationParam.MAX_UNLIMITED; + } + } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) { + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = OperationParam.MAX_UNLIMITED; + } + } else { + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = 1; + } + } + + boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers()); + + boolean isSearchParam = IQueryParameterType.class.isAssignableFrom(myParameterType) + || IQueryParameterOr.class.isAssignableFrom(myParameterType) + || IQueryParameterAnd.class.isAssignableFrom(myParameterType); + + /* + * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also + * extend this interface. I'm not sure if they should in the end.. but they do, so we + * exclude them. + */ + isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); + + myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) + || String.class.equals(myParameterType) + || isSearchParam + || ValidationModeEnum.class.equals(myParameterType); + + final boolean isAnnotationPresent = myParameterType.isAnnotationPresent(OperationEmbeddedType.class); + + /* + * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We + * should probably clean this up.. + */ + if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { + // LUKETODO: this is where we get the Exception: add an else if + if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { + myParamType = "Resource"; + } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { + myParamType = "Reference"; + myAllowGet = true; + } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { + myParamType = "Coding"; + myAllowGet = true; + } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) { + myParamType = "date"; + myMax = 2; + myAllowGet = true; + } else if (myParameterType.equals(ValidationModeEnum.class)) { + myParamType = "code"; + } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) { + myParamType = myContext + .getElementDefinition((Class) myParameterType) + .getName(); + } else if (isSearchParam) { + myParamType = "string"; + mySearchParameterBinding = new SearchParameter(myName, myMin > 0); + mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES); + mySearchParameterBinding.setType( + myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + myConverter = new OperationParamConverter(); + } else { + throw new ConfigurationException(Msg.code(361) + "Invalid type for @OperationParam on method " + + theMethod + ": " + myParameterType.getName()); + } + } + } + + public static void validateTypeIsAppropriateVersionForContext( + Method theMethod, Class theParameterType, FhirContext theContext, String theUseDescription) { + if (theParameterType != null) { + if (theParameterType.isInterface()) { + // TODO: we could probably be a bit more nuanced here but things like + // IBaseResource are often used and they aren't version specific + return; + } + + FhirVersionEnum elementVersion = FhirVersionEnum.determineVersionForType(theParameterType); + if (elementVersion != null) { + if (elementVersion != theContext.getVersion().getVersion()) { + throw new ConfigurationException(Msg.code(360) + "Incorrect use of type " + + theParameterType.getSimpleName() + " as " + theUseDescription + + " type for method when theContext is for version " + + theContext.getVersion().getVersion().name() + " in method: " + theMethod.toString()); + } + } + } + } + + OperationEmbeddedParameter setConverter(IOperationParamConverter theConverter) { + myConverter = theConverter; + return this; + } + + private void throwWrongParamType(Object nextValue) { + throw new InvalidRequestException(Msg.code(362) + "Request has parameter " + myName + " of type " + + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); + } + + @SuppressWarnings("unchecked") + @Override + public Object translateQueryParametersIntoServerArgument( + RequestDetails theRequest, BaseMethodBinding theMethodBinding) + throws InternalErrorException, InvalidRequestException { + List matchingParamValues = new ArrayList<>(); + + OperationMethodBinding method = (OperationMethodBinding) theMethodBinding; + + if (theRequest.getRequestType() == RequestTypeEnum.GET + || method.isManualRequestMode() + || method.isDeleteEnabled()) { + translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues); + } else { + translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues); + } + + if (matchingParamValues.isEmpty()) { + return null; + } + + if (myInnerCollectionType == null) { + return matchingParamValues.get(0); + } + + Collection retVal = ReflectionUtil.newInstance(myInnerCollectionType); + retVal.addAll(matchingParamValues); + return retVal; + } + + private void translateQueryParametersIntoServerArgumentForGet( + RequestDetails theRequest, List matchingParamValues) { + if (mySearchParameterBinding != null) { + + List params = new ArrayList<>(); + String nameWithQualifierColon = myName + ":"; + + for (String nextParamName : theRequest.getParameters().keySet()) { + String qualifier; + if (nextParamName.equals(myName)) { + qualifier = null; + } else if (nextParamName.startsWith(nameWithQualifierColon)) { + qualifier = nextParamName.substring(nextParamName.indexOf(':')); + } else { + // This is some other parameter, not the one bound by this instance + continue; + } + String[] values = theRequest.getParameters().get(nextParamName); + if (values != null) { + for (String nextValue : values) { + params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); + } + } + } + if (!params.isEmpty()) { + for (QualifiedParamList next : params) { + Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); + addValueToList(matchingParamValues, values); + } + } + + } else { + String[] paramValues = theRequest.getParameters().get(myName); + if (paramValues != null && paramValues.length > 0) { + if (myAllowGet) { + + if (DateRangeParam.class.isAssignableFrom(myParameterType)) { + List parameters = new ArrayList<>(); + parameters.add(QualifiedParamList.singleton(paramValues[0])); + if (paramValues.length > 1) { + parameters.add(QualifiedParamList.singleton(paramValues[1])); + } + DateRangeParam dateRangeParam = new DateRangeParam(); + FhirContext ctx = theRequest.getServer().getFhirContext(); + dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters); + matchingParamValues.add(dateRangeParam); + + } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { + + processAllCommaSeparatedValues(paramValues, t -> { + IBaseReference param = (IBaseReference) ReflectionUtil.newInstance(myParameterType); + param.setReference(t); + matchingParamValues.add(param); + }); + + } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { + + processAllCommaSeparatedValues(paramValues, t -> { + TokenParam tokenParam = new TokenParam(); + tokenParam.setValueAsQueryToken(myContext, myName, null, t); + + IBaseCoding param = (IBaseCoding) ReflectionUtil.newInstance(myParameterType); + param.setSystem(tokenParam.getSystem()); + param.setCode(tokenParam.getValue()); + matchingParamValues.add(param); + }); + + } else if (String.class.isAssignableFrom(myParameterType)) { + + matchingParamValues.addAll(Arrays.asList(paramValues)); + + } else if (ValidationModeEnum.class.equals(myParameterType)) { + + if (isNotBlank(paramValues[0])) { + ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); + if (validationMode != null) { + matchingParamValues.add(validationMode); + } else { + throwInvalidMode(paramValues[0]); + } + } + + } else { + for (String nextValue : paramValues) { + FhirContext ctx = theRequest.getServer().getFhirContext(); + RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) + ctx.getElementDefinition(myParameterType.asSubclass(IBase.class)); + IPrimitiveType instance = def.newInstance(); + instance.setValueAsString(nextValue); + matchingParamValues.add(instance); + } + } + } else { + HapiLocalizer localizer = + theRequest.getServer().getFhirContext().getLocalizer(); + String msg = localizer.getMessage( + OperationEmbeddedParameter.class, "urlParamNotPrimitive", myOperationName, myName); + throw new MethodNotAllowedException(Msg.code(363) + msg, RequestTypeEnum.POST); + } + } + } + } + + /** + * This method is here to mediate between the POST form of operation parameters (i.e. elements within a Parameters + * resource) and the GET form (i.e. URL parameters). + *

+ * Essentially we want to allow comma-separated values as is done with searches on URLs. + *

+ */ + private void processAllCommaSeparatedValues(String[] theParamValues, Consumer theHandler) { + for (String nextValue : theParamValues) { + QualifiedParamList qualifiedParamList = + QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextValue); + for (String nextSplitValue : qualifiedParamList) { + theHandler.accept(nextSplitValue); + } + } + } + + private void translateQueryParametersIntoServerArgumentForPost( + RequestDetails theRequest, List matchingParamValues) { + IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); + if (requestContents != null) { + RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); + if (def.getName().equals("Parameters")) { + + BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); + BaseRuntimeElementCompositeDefinition paramChildElem = + (BaseRuntimeElementCompositeDefinition) paramChild.getChildByName("parameter"); + + RuntimeChildPrimitiveDatatypeDefinition nameChild = + (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name"); + BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]"); + BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource"); + + IAccessor paramChildAccessor = paramChild.getAccessor(); + List values = paramChildAccessor.getValues(requestContents); + for (IBase nextParameter : values) { + List nextNames = nameChild.getAccessor().getValues(nextParameter); + if (nextNames != null && !nextNames.isEmpty()) { + IPrimitiveType nextName = (IPrimitiveType) nextNames.get(0); + if (myName.equals(nextName.getValueAsString())) { + + if (myParameterType.isAssignableFrom(nextParameter.getClass())) { + matchingParamValues.add(nextParameter); + } else { + List paramValues = + valueChild.getAccessor().getValues(nextParameter); + List paramResources = + resourceChild.getAccessor().getValues(nextParameter); + if (paramValues != null && !paramValues.isEmpty()) { + tryToAddValues(paramValues, matchingParamValues); + } else if (paramResources != null && !paramResources.isEmpty()) { + tryToAddValues(paramResources, matchingParamValues); + } + } + } + } + } + + } else { + + if (myParameterType.isAssignableFrom(requestContents.getClass())) { + tryToAddValues(Arrays.asList(requestContents), matchingParamValues); + } + } + } + } + + @SuppressWarnings("unchecked") + private void tryToAddValues(List theParamValues, List theMatchingParamValues) { + for (Object nextValue : theParamValues) { + if (nextValue == null) { + continue; + } + if (myConverter != null) { + nextValue = myConverter.incomingServer(nextValue); + } + if (myParameterType.equals(String.class)) { + if (nextValue instanceof IPrimitiveType) { + IPrimitiveType source = (IPrimitiveType) nextValue; + theMatchingParamValues.add(source.getValueAsString()); + continue; + } + } + if (!myParameterType.isAssignableFrom(nextValue.getClass())) { + Class sourceType = (Class) nextValue.getClass(); + Class targetType = (Class) myParameterType; + BaseRuntimeElementDefinition sourceTypeDef = myContext.getElementDefinition(sourceType); + BaseRuntimeElementDefinition targetTypeDef = myContext.getElementDefinition(targetType); + if (targetTypeDef instanceof IRuntimeDatatypeDefinition + && sourceTypeDef instanceof IRuntimeDatatypeDefinition) { + IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef; + if (targetTypeDtDef.isProfileOf(sourceType)) { + FhirTerser terser = myContext.newTerser(); + IBase newTarget = targetTypeDef.newInstance(); + terser.cloneInto((IBase) nextValue, newTarget, true); + theMatchingParamValues.add(newTarget); + continue; + } + } + throwWrongParamType(nextValue); + } + + addValueToList(theMatchingParamValues, nextValue); + } + } + + public String getDescription() { + return myDescription; + } + + public List getExampleValues() { + return myExampleValues; + } + + interface IOperationParamConverter { + + Object incomingServer(Object theObject); + + Object outgoingClient(Object theObject); + } + + class OperationParamConverter implements IOperationParamConverter { + + public OperationParamConverter() { + Validate.isTrue(mySearchParameterBinding != null); + } + + @Override + public Object incomingServer(Object theObject) { + IPrimitiveType obj = (IPrimitiveType) theObject; + List paramList = Collections.singletonList( + QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); + return mySearchParameterBinding.parse(myContext, paramList); + } + + @Override + public Object outgoingClient(Object theObject) { + IQueryParameterType obj = (IQueryParameterType) theObject; + IPrimitiveType retVal = + (IPrimitiveType) myContext.getElementDefinition("string").newInstance(); + retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); + return retVal; + } + } + + public static void throwInvalidMode(String paramValues) { + throw new InvalidRequestException(Msg.code(364) + "Invalid mode value: \"" + paramValues + "\""); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 464f6f52e748..da8d85571ffb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -69,6 +69,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; +// LUKETODO: DO NOT HIJACK THIS +// LUKETODO: clone this and use this for Embedded object params +// LUKETODO: like EmbeddedOperationParameter public class OperationParameter implements IParameter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationParameter.class); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 6f575083744f..97e51b99400c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -31,12 +31,16 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Parameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; public class CareGapsOperationProvider { + private static final Logger ourLog = LoggerFactory.getLogger(MeasureOperationsProvider.class); + private final ICareGapsServiceFactory myR4CareGapsProcessorFactory; private final StringTimePeriodHandler myStringTimePeriodHandler; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 22d0704dc5c9..31019e18f7b4 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.cr.common.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; import ca.uhn.fhir.rest.annotation.IdParam; @@ -27,11 +28,11 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import org.hl7.fhir.OperationOutcome; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; @@ -116,16 +117,22 @@ public MeasureReport evaluateMeasure( // } @Operation(name = "$fooBar", manualResponse = true, idempotent = true) + // LUKETODO: consider defining a new @OperationEmbeddedParam public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { ourLog.info("1234: fooBar params: {}", theParams); } -// @Operation(name = "$returnsOutcome", manualResponse = true, idempotent = true) -// public OperationOutcome returnsOutcome(@OperationParam(name = "params") ReturnsOutcomeParams theParams) { -// ourLog.info("1234: returnsOutcome params: {}", theParams); -// -// return new OperationOutcome(); -// } + @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) + public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { + final Bundle bundle = new Bundle(); + bundle.setIdentifier(new Identifier().setValue("aValue")); + + final String bundleString = FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); + + ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); + + return bundle; + } void example() { fooBar(new FooBarParams(null, null)); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java similarity index 55% rename from hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java rename to hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java index 99a2634e45a4..021513e14bc5 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsOutcomeParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java @@ -3,17 +3,17 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; -import java.util.List; import java.util.StringJoiner; @OperationEmbeddedType -public class ReturnsOutcomeParams { - @OperationParam(name = "oneString") +public class ReturnsBundleParams { + @OperationParam(name = "string") // LUKETODO: do I always need to make it a BooleanType and not a Boolean? private String myString; - @OperationParam(name = "multStrings") - private List myStrings; + public ReturnsBundleParams(String myString) { + this.myString = myString; + } public String getString() { return myString; @@ -23,19 +23,10 @@ public void setString(String myString) { this.myString = myString; } - public List getStrings() { - return myStrings; - } - - public void setStrings(List myStrings) { - this.myStrings = myStrings; - } - @Override public String toString() { - return new StringJoiner(", ", ReturnsOutcomeParams.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", ReturnsBundleParams.class.getSimpleName() + "[", "]") .add("myString='" + myString + "'") - .add("myStrings=" + myStrings) .toString(); } } From 18b08a70131f75edd184cd74c7f85b1c8f8ec550 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 11:30:37 -0500 Subject: [PATCH 05/75] Changes to set the table for parallel evaluate-measure and care-gaps operations using params. --- .../ca/uhn/fhir/rest/annotation/IdParam.java | 2 +- .../annotation/OperationEmbeddedParam.java | 97 +++++++++++++++++++ .../ca/uhn/fhir/rest/param/ParameterUtil.java | 40 +++++++- .../rest/server/method/BaseMethodBinding.java | 1 + .../method/OperationEmbeddedTypeUtils.java | 6 ++ .../server/provider/ProviderConstants.java | 2 + .../uhn/fhir/cr/r4/measure/FooBarParams.java | 9 +- .../r4/measure/MeasureOperationsProvider.java | 29 ++++++ 8 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/IdParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/IdParam.java index cb81e8d0263b..95ff65f87738 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/IdParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/IdParam.java @@ -25,7 +25,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) +@Target({ElementType.PARAMETER, ElementType.FIELD}) public @interface IdParam { /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java new file mode 100644 index 000000000000..3cffe0da0f3d --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -0,0 +1,97 @@ +/* + * #%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 ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.param.StringParam; +import org.hl7.fhir.instance.model.api.IBase; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) +// LUKETODO: javadoc to make this clear that it's associated with an OperationEmbeddedParameter +public @interface OperationEmbeddedParam { + + /** + * Value for {@link OperationEmbeddedParam#max()} indicating no maximum + */ + int MAX_UNLIMITED = -1; + + /** + * Value for {@link OperationEmbeddedParam#max()} indicating that the maximum will be inferred + * from the type. If the type is a single parameter type (e.g. StringDt, + * TokenParam, IBaseResource) the maximum will be + * 1. + *

+ * If the type is a collection, e.g. + * List<StringDt> or List<TokenOrListParam> + * the maximum will be set to *. If the param is a search parameter + * "and" type, such as TokenAndListParam the maximum will also be + * set to * + *

+ * + * @since 1.5 + */ + int MAX_DEFAULT = -2; + + /** + * The name of the parameter + */ + String name(); + + /** + * The type of the parameter. This will only have effect on @OperationParam + * annotations specified as values for {@link Operation#returnParameters()}, otherwise the + * value will be ignored. Value should be one of: + *
    + *
  • A resource type, e.g. Patient.class
  • + *
  • A datatype, e.g. {@link StringDt}.class or CodeableConceptDt.class + *
  • A RESTful search parameter type, e.g. {@link StringParam}.class + *
+ */ + Class type() default IBase.class; + + /** + * Optionally specifies the type of the parameter as a string, such as Coding or + * base64Binary. This can be useful if you want to use a generic interface type + * on the actual method,such as {@link org.hl7.fhir.instance.model.api.IPrimitiveType} or + * {@link @org.hl7.fhir.instance.model.api.ICompositeType}. + */ + String typeName() default ""; + + /** + * The minimum number of repetitions allowed for this child (default is 0) + */ + int min() default 0; + + /** + * The maximum number of repetitions allowed for this child. Should be + * set to {@link #MAX_UNLIMITED} if there is no limit to the number of + * repetitions. See {@link #MAX_DEFAULT} for a description of the default + * behaviour. + */ + int max() default MAX_DEFAULT; +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 36f64ae7dde8..3cf5626f2323 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -27,16 +27,19 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.api.Constants; 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; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -131,10 +134,7 @@ public static Integer findIdParameterIndex(Method theMethod, FhirContext theCont return index; } - // public static Integer findSinceParameterIndex(Method theMethod) { - // return findParamIndex(theMethod, Since.class); - // } - + @Nullable public static Integer findParamAnnotationIndex(Method theMethod, Class toFind) { int paramIndex = 0; for (Annotation[] annotations : theMethod.getParameterAnnotations()) { @@ -149,6 +149,38 @@ public static Integer findParamAnnotationIndex(Method theMethod, Class toFind return null; } + @Nullable + public static Integer findParamAnnotationIndexFromEmbedded(Method theMethod, Class toFind) { + final Class[] parameterTypes = theMethod.getParameterTypes(); + + // LUKETODO: be mindful if we get rid of OperationEmbeddedType + final long operationEmbeddedTypesCount = Arrays.stream(parameterTypes) + .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) + .count(); + + if (operationEmbeddedTypesCount > 1) { + throw new ConfigurationException(String.format("%sMore than one parameter with OperationEmbeddedType for method: %s", Msg.code(99999), theMethod.getName())); + } + + if (operationEmbeddedTypesCount == 0) { + return null; + } + + int paramIndex = 0; + for (Annotation[] annotations : theMethod.getParameterAnnotations()) { + for (Annotation nextAnnotation : annotations) { + Class class1 = nextAnnotation.annotationType(); + if (toFind.isAssignableFrom(class1)) { + return paramIndex; + } + } + paramIndex++; + } + + return null; + } + + @Nullable public static Object fromInteger(Class theType, IntegerDt theArgument) { if (theArgument == null) { return null; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 30e31522700c..ddb0cfefb889 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -328,6 +328,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final Object operationEmbeddedType = constructor.newInstance(theMethodParams); ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); + // LUKETODO: design for future use factory methods return method.invoke(getProvider(), operationEmbeddedType); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java new file mode 100644 index 000000000000..9659f8321d9f --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java @@ -0,0 +1,6 @@ +package ca.uhn.fhir.rest.server.method; + +// LUKETODO: common methods used for this stuff +// LUKETODO: Spring, if applicable +public class OperationEmbeddedTypeUtils { +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 258fffae774b..1b6dae48b91e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -112,8 +112,10 @@ public class ProviderConstants { * Clinical Reasoning Operations */ public static final String CR_OPERATION_EVALUATE_MEASURE = "$evaluate-measure"; + public static final String CR_OPERATION_EVALUATE_MEASURE_2 = "$evaluate-measure2"; public static final String CR_OPERATION_CARE_GAPS = "$care-gaps"; + public static final String CR_OPERATION_CARE_GAPS_2 = "$care-gaps2"; public static final String CR_OPERATION_SUBMIT_DATA = "$submit-data"; public static final String CR_OPERATION_EVALUATE = "$evaluate"; public static final String CR_OPERATION_CQL = "$cql"; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java index 1833bbf05f4f..0ef1222fe4c3 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java @@ -35,6 +35,7 @@ **/ @OperationEmbeddedType public class FooBarParams { + // LUKETODO: use OperationEmbeddedParameter instead @OperationParam(name = "doFoo") // LUKETODO: do I always need to make it a BooleanType and not a Boolean? private BooleanType myDoFoo; @@ -51,19 +52,11 @@ public BooleanType getDoFoo() { return myDoFoo; } - public void setDoFoo(BooleanType theDoFoo) { - myDoFoo = theDoFoo; - } - // LUKETODO: do I always need to make it a IntegerType and not an Integer? public IntegerType getCount() { return myCount; } - public void setCount(IntegerType theCount) { - myCount = theCount; - } - @Override public String toString() { return new StringJoiner(", ", FooBarParams.class.getSimpleName() + "[", "]") diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 31019e18f7b4..ab521d63b2c0 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -110,6 +110,35 @@ public MeasureReport evaluateMeasure( thePractitioner); } + @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE_2, idempotent = true, type = Measure.class) + public MeasureReport evaluateMeasure2( + EvaluateMeasureSingleParams theParams, + RequestDetails theRequestDetails) + throws InternalErrorException, FHIRException { + // LUKETODO: Parameters within Parameters + return myR4MeasureServiceFactory + .create(theRequestDetails) + .evaluate( + // LUKETODO: 1. can we support the concept of Either in hapi-fhir annotations? + // LUKETODO: 2. can we modify OperationParam to support the concept of mututally exclusive params + // LUKETODO: 3. code gen from operation definition + Eithers.forMiddle3(theParams.getId()), + // LUKETODO: push this into the hapi-fhir REST framework code + myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), + // LUKETODO: push this into the hapi-fhir REST framework code + myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + theParams.getReportType(), + theParams.getSubject(), + theParams.getLastReceivedOn(), + null, + theParams.getTerminologyEndpoint(), + null, + theParams.getAdditionalData(), + theParams.getParameters(), + theParams.getProductLine(), + theParams.getPractitioner()); + } + // @Operation(name = "$fooBar", manualResponse = true, idempotent = true) // OperationOutcome fooBar(FooBarParams theParams) { // ourLog.info("fooBar params: {}", theParams); From 0e783d968c2038f0d151ad5ef7ec8ddfcc794a45 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 11:34:27 -0500 Subject: [PATCH 06/75] Add params classes. --- .../fhir/cr/r4/measure/CareGapsParams.java | 94 +++++++++++++++ .../measure/EvaluateMeasureSingleParams.java | 112 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java create mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java new file mode 100644 index 000000000000..48c56dd71a27 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -0,0 +1,94 @@ +package ca.uhn.fhir.cr.r4.measure; + +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CanonicalType; + +import java.util.List; +import java.util.StringJoiner; + +@OperationEmbeddedType +public class CareGapsParams { + @OperationParam(name = "periodStart") + private final String myPeriodStart; + @OperationParam(name = "periodEnd") + private final String myPeriodEnd; + @OperationParam(name = "subject") + private final String mySubject; + @OperationParam(name = "status") + private final List myStatus; + @OperationParam(name = "measureId") + private final List myMeasureId; + @OperationParam(name = "measureIdentifier") + private final List myMeasureIdentifier; + @OperationParam(name = "measureUrl") + private final List myMeasureUrl; + @OperationParam(name = "nonDocument") + private final BooleanType myNonDocument; + + public CareGapsParams( + String myPeriodStart, + String myPeriodEnd, + String mySubject, + List myStatus, + List myMeasureId, + List myMeasureIdentifier, + List myMeasureUrl, + BooleanType myNonDocument) { + this.myPeriodStart = myPeriodStart; + this.myPeriodEnd = myPeriodEnd; + this.mySubject = mySubject; + this.myStatus = myStatus; + this.myMeasureId = myMeasureId; + this.myMeasureIdentifier = myMeasureIdentifier; + this.myMeasureUrl = myMeasureUrl; + this.myNonDocument = myNonDocument; + } + + public String getPeriodStart() { + return myPeriodStart; + } + + public String getPeriodEnd() { + return myPeriodEnd; + } + + public String getSubject() { + return mySubject; + } + + public List getStatus() { + return myStatus; + } + + public List getMeasureId() { + return myMeasureId; + } + + public List getMeasureIdentifier() { + return myMeasureIdentifier; + } + + public List getMeasureUrl() { + return myMeasureUrl; + } + + public BooleanType getNonDocument() { + return myNonDocument; + } + + @Override + public String toString() { + return new StringJoiner(", ", CareGapsParams.class.getSimpleName() + "[", "]") + .add("myPeriodStart='" + myPeriodStart + "'") + .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("mySubject='" + mySubject + "'") + .add("myStatus=" + myStatus) + .add("myMeasureId=" + myMeasureId) + .add("myMeasureIdentifier=" + myMeasureIdentifier) + .add("myMeasureUrl=" + myMeasureUrl) + .add("myNonDocument=" + myNonDocument) + .toString(); + } +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java new file mode 100644 index 000000000000..937da6549249 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -0,0 +1,112 @@ +package ca.uhn.fhir.cr.r4.measure; + +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; + +import java.util.StringJoiner; + +@OperationEmbeddedType +public class EvaluateMeasureSingleParams { + @IdParam + private final IdType myId; + @OperationParam(name = "periodStart") + private final String myPeriodStart; + @OperationParam(name = "periodEnd") + private final String myPeriodEnd; + @OperationParam(name = "reportType") + private final String myReportType; + @OperationParam(name = "subject") + private final String mySubject; + @OperationParam(name = "practitioner") + private final String myPractitioner; + @OperationParam(name = "lastReceivedOn") + private final String myLastReceivedOn; + @OperationParam(name = "productLine") + private final String myProductLine; + @OperationParam(name = "additionalData") + private final Bundle myAdditionalData; + @OperationParam(name = "terminologyEndpoint") + private final Endpoint myTerminologyEndpoint; + @OperationParam(name = "parameters") + private final Parameters myParameters; + + public EvaluateMeasureSingleParams(IdType myId, String myPeriodStart, String myPeriodEnd, String myReportType, String mySubject, String myPractitioner, String myLastReceivedOn, String myProductLine, Bundle myAdditionalData, Endpoint myTerminologyEndpoint, Parameters myParameters) { + this.myId = myId; + this.myPeriodStart = myPeriodStart; + this.myPeriodEnd = myPeriodEnd; + this.myReportType = myReportType; + this.mySubject = mySubject; + this.myPractitioner = myPractitioner; + this.myLastReceivedOn = myLastReceivedOn; + this.myProductLine = myProductLine; + this.myAdditionalData = myAdditionalData; + this.myTerminologyEndpoint = myTerminologyEndpoint; + this.myParameters = myParameters; + } + + public IdType getId() { + return myId; + } + + public String getPeriodStart() { + return myPeriodStart; + } + + public String getPeriodEnd() { + return myPeriodEnd; + } + + public String getReportType() { + return myReportType; + } + + public String getSubject() { + return mySubject; + } + + public String getPractitioner() { + return myPractitioner; + } + + public String getLastReceivedOn() { + return myLastReceivedOn; + } + + public String getProductLine() { + return myProductLine; + } + + public Bundle getAdditionalData() { + return myAdditionalData; + } + + public Endpoint getTerminologyEndpoint() { + return myTerminologyEndpoint; + } + + public Parameters getParameters() { + return myParameters; + } + + @Override + public String toString() { + return new StringJoiner(", ", EvaluateMeasureSingleParams.class.getSimpleName() + "[", "]") + .add("myId=" + myId) + .add("myPeriodStart='" + myPeriodStart + "'") + .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("myReportType='" + myReportType + "'") + .add("mySubject='" + mySubject + "'") + .add("myPractitioner='" + myPractitioner + "'") + .add("myLastReceivedOn='" + myLastReceivedOn + "'") + .add("myProductLine='" + myProductLine + "'") + .add("myAdditionalData=" + myAdditionalData) + .add("myTerminologyEndpoint=" + myTerminologyEndpoint) + .add("myParameters=" + myParameters) + .toString(); + } +} From 83a92b4ae6a0da2883e36910b922b936e2e7b724 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 13:23:51 -0500 Subject: [PATCH 07/75] Cleanup MethodUtil. Replace OperationParam with OperationEmbeddedParam. Add new params classes. Delete obsolete params classes. Add cloned operation params. --- .../fhir/rest/annotation/OperationParam.java | 3 +- .../fhir/rest/server/method/MethodUtil.java | 183 ++++++++---------- .../r4/measure/CareGapsOperationProvider.java | 24 +++ .../fhir/cr/r4/measure/CareGapsParams.java | 18 +- .../measure/EvaluateMeasureSingleParams.java | 22 +-- .../uhn/fhir/cr/r4/measure/FooBarParams.java | 5 +- .../r4/measure/MeasureOperationsProvider.java | 24 +-- .../cr/r4/measure/ReturnsBundleParams.java | 32 --- 8 files changed, 141 insertions(+), 170 deletions(-) delete mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index 49d07cc52fa8..747e7bc0da59 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -31,8 +31,7 @@ /** */ @Retention(RetentionPolicy.RUNTIME) -@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) -// LUKETODO: 1-1 with OperationParameter (subclass of IParameter) +@Target(value = ElementType.PARAMETER) public @interface OperationParam { /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 9dc1de60d319..1c83649b61f6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -37,6 +37,7 @@ import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; @@ -105,6 +106,7 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] @SuppressWarnings("unchecked") public static List getResourceParameters( final FhirContext theContext, Method theMethod, Object theProvider) { + ourLog.info("1234: getResourceParameters: " + theMethod.getName()); List parameters = new ArrayList<>(); // LUKETODO: why no caregaps here???? @@ -113,89 +115,97 @@ public static List getResourceParameters( // LUKETODO: one param per method parameter: what happens if we expand this? // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - if (Arrays.stream(parameterTypes) - .anyMatch(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class))) { + final List> operationEmbeddedTypes = Arrays.stream(parameterTypes) + .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) + .collect(Collectors.toUnmodifiableList()); + + if (! operationEmbeddedTypes.isEmpty()) { ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); // This is the @Operation parameter on the method itself (ex: evaluateMeasure) final Operation op = theMethod.getAnnotation(Operation.class); - if (parameterTypes.length > 1) { + if (operationEmbeddedTypes.size() > 1) { // LUKETODO: error throw new ConfigurationException( - Msg.code(99999) + "Only one OperationEmbeddedType is supported for now!"); + String.format("%sOnly one OperationEmbeddedType is supported for now for method: %s", Msg.code(99999), theMethod.getName())); } - final Class operationEmbeddedType = parameterTypes[0]; - - final Field[] fields = operationEmbeddedType.getDeclaredFields(); - ourLog.info("1234: declaredFields: {}", fields.length); - for (Field field : fields) { - final String fieldName = field.getName(); - final Class fieldType = field.getType(); - final Annotation[] fieldAnnotations = field.getAnnotations(); - - final Set annotationClassNames = Arrays.stream(fieldAnnotations) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(Collectors.toUnmodifiableSet()); - - ourLog.info( - "1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, fieldAnnotations: {}", - fieldName, - fieldType.getName(), - annotationClassNames); - - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(Msg.code(99999) + "More than one annotation per field!"); - } - - // This is the parameter on the field in question on the OperationEmbeddedType class: ex myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; + // LUKETODO: handle multiple RequestDetails with an error + for (Class parameterType : parameterTypes) { final IParameter param; + // If either the first or second parameter is a RequestDetails, handle it + if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { + parameters.add(new RequestDetailsParameter()); + } else { // LUKETODO: specific check here? + // LUKETODO: limit to a single Params object + final Field[] fields = parameterType.getDeclaredFields(); + + for (Field field : fields) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final Annotation[] fieldAnnotations = field.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format("%sNo annotations for field: %s for method: %s", Msg.code(99999), fieldName, theMethod.getName())); + } - // LUKETODO: what if this is not a IdParam or an OperationParam? - if (fieldAnnotation instanceof IdParam) { - param = new NullParameter(); - } else if (fieldAnnotation instanceof OperationParam) { - final OperationParam operationParam = (OperationParam) fieldAnnotation; - - final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning repo -// final OperationParameter operationParameter = new OperationParameter( - final OperationEmbeddedParameter operationParameter = new OperationEmbeddedParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - - // Not sure what these are, but I think they're for params that are part of a Collection parameter - // and may have soemthing to do with a SearchParameter - final Class> outerCollectionType = null; - final Class> innerCollectionType = null; - - operationParameter.initializeTypes(theMethod, outerCollectionType, innerCollectionType, fieldType); - - param = operationParameter; - } else { - throw new ConfigurationException(Msg.code(99999) + "Unsupport param fieldType: " + fieldAnnotation); - } - - // LUKETODO: somehow add multiples of parameters in this block - - // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType - // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format("%sMore than one annotation for field: %s for method: %s", Msg.code(99999), fieldName, theMethod.getName())); + } - parameters.add(param); + final Set annotationClassNames = Arrays.stream(fieldAnnotations) + .map(Annotation::annotationType) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); + + ourLog.info( + "1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, fieldAnnotations: {}", + fieldName, + fieldType.getName(), + annotationClassNames); + + + // This is the parameter on the field in question on the OperationEmbeddedType class: ex myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + // LUKETODO: what if this is not a IdParam or an OperationParam? + if (fieldAnnotation instanceof IdParam) { + parameters.add(new NullParameter()); + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + // LUKETODO: use OperationEmbeddedParam instead + final OperationEmbeddedParam operationParam = (OperationEmbeddedParam) fieldAnnotation; + + final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning repo + final OperationEmbeddedParameter operationParameter = new OperationEmbeddedParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + + // Not sure what these are, but I think they're for params that are part of a Collection parameter + // and may have soemthing to do with a SearchParameter + final Class> outerCollectionType = null; + final Class> innerCollectionType = null; + + operationParameter.initializeTypes(theMethod, outerCollectionType, innerCollectionType, fieldType); + + parameters.add(operationParameter); + } else { + throw new ConfigurationException(Msg.code(99999) + "Unsupported param fieldType: " + fieldAnnotation); + } + } + } } // LUKETODO: short-circuit for now @@ -214,38 +224,7 @@ public static List getResourceParameters( // TagList is handled directly within the method bindings param = new NullParameter(); } else { - // LUKETODO: add comments about what this does - if (parameterType.isAnnotationPresent(OperationEmbeddedType.class)) { - // ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); - // - // final Field[] fields = parameterType.getDeclaredFields(); - // ourLog.info("1234: declaredFields: {}", fields.length); - // ourLog.info("1234: MethodUtil: nextParameterAnnotations: {}", - // Arrays.toString(nextParameterAnnotations)); - // for (Field field : fields) { - // final String fieldName = field.getName(); - // final Class type = field.getType(); - // final Annotation[] annotations = field.getAnnotations(); - // - // final Set annotationClassNames = - // Arrays.stream(annotations).map(Annotation::annotationType).map(Class::getName).collect(Collectors.toUnmodifiableSet()); - // - // ourLog.info("1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, - // annotations: {}", fieldName, type.getName(), annotationClassNames); - // - // // LUKETODO: somehow add multiples of parameters in this block - // - // // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType - // // LUKETODO: DO NOT ALLOW an OperationEmbeddedType within an OperationEmbeddedType - // } - // - // // LUKETODO: figure out the pattern here and possibly reuse it: - // // LUKETODO: take the original parameterType and see if it's a Collection - // // LUKETODO: take the generic parameter type for the Collection, and assign it to - // parameterType - // // LUKETODO: as a sort of guard, if the parametter if null, then get the superclass for the - // method, get the superclass method, and then get the generic type for the superclass method??? - } else if (Collection.class.isAssignableFrom(parameterType)) { + if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 97e51b99400c..5c7e4a5e8b8f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -124,4 +124,28 @@ public Parameters careGapsReport( .map(BooleanType::getValue) .orElse(false)); } + +// @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) +// public Parameters careGapsReport2( +// // LUKETODO: include RequestDetails in Params object? +// RequestDetails theRequestDetails, +// @OperationParam(name = "params") CareGapsParams theParams) { +// +// return myR4CareGapsProcessorFactory +// .create(theRequestDetails) +// .getCareGapsReport( +// // LUKETODO: how to handle passing this down seamlessly? +// myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), +// myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), +// theParams.getSubject(), +// theParams.getStatus(), +// theParams.getMeasureId() == null +// ? null +// : theParams.getMeasureId().stream().map(IdType::new).collect(Collectors.toList()), +// theParams.getMeasureIdentifier(), +// theParams.getMeasureUrl(), +// Optional.ofNullable(theParams.getNonDocument()) +// .map(BooleanType::getValue) +// .orElse(false)); +// } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 48c56dd71a27..06333a9eee49 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; -import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; @@ -10,21 +10,21 @@ @OperationEmbeddedType public class CareGapsParams { - @OperationParam(name = "periodStart") + @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; - @OperationParam(name = "periodEnd") + @OperationEmbeddedParam(name = "periodEnd") private final String myPeriodEnd; - @OperationParam(name = "subject") + @OperationEmbeddedParam(name = "subject") private final String mySubject; - @OperationParam(name = "status") + @OperationEmbeddedParam(name = "status") private final List myStatus; - @OperationParam(name = "measureId") + @OperationEmbeddedParam(name = "measureId") private final List myMeasureId; - @OperationParam(name = "measureIdentifier") + @OperationEmbeddedParam(name = "measureIdentifier") private final List myMeasureIdentifier; - @OperationParam(name = "measureUrl") + @OperationEmbeddedParam(name = "measureUrl") private final List myMeasureUrl; - @OperationParam(name = "nonDocument") + @OperationEmbeddedParam(name = "nonDocument") private final BooleanType myNonDocument; public CareGapsParams( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 937da6549249..78e93052a0f8 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -2,7 +2,7 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; -import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; @@ -14,25 +14,25 @@ public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; - @OperationParam(name = "periodStart") + @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; - @OperationParam(name = "periodEnd") + @OperationEmbeddedParam(name = "periodEnd") private final String myPeriodEnd; - @OperationParam(name = "reportType") + @OperationEmbeddedParam(name = "reportType") private final String myReportType; - @OperationParam(name = "subject") + @OperationEmbeddedParam(name = "subject") private final String mySubject; - @OperationParam(name = "practitioner") + @OperationEmbeddedParam(name = "practitioner") private final String myPractitioner; - @OperationParam(name = "lastReceivedOn") + @OperationEmbeddedParam(name = "lastReceivedOn") private final String myLastReceivedOn; - @OperationParam(name = "productLine") + @OperationEmbeddedParam(name = "productLine") private final String myProductLine; - @OperationParam(name = "additionalData") + @OperationEmbeddedParam(name = "additionalData") private final Bundle myAdditionalData; - @OperationParam(name = "terminologyEndpoint") + @OperationEmbeddedParam(name = "terminologyEndpoint") private final Endpoint myTerminologyEndpoint; - @OperationParam(name = "parameters") + @OperationEmbeddedParam(name = "parameters") private final Parameters myParameters; public EvaluateMeasureSingleParams(IdType myId, String myPeriodStart, String myPeriodEnd, String myReportType, String mySubject, String myPractitioner, String myLastReceivedOn, String myProductLine, Bundle myAdditionalData, Endpoint myTerminologyEndpoint, Parameters myParameters) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java index 0ef1222fe4c3..07f47632d88b 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import org.hl7.fhir.r4.model.BooleanType; @@ -36,11 +37,11 @@ @OperationEmbeddedType public class FooBarParams { // LUKETODO: use OperationEmbeddedParameter instead - @OperationParam(name = "doFoo") + @OperationEmbeddedParam(name = "doFoo") // LUKETODO: do I always need to make it a BooleanType and not a Boolean? private BooleanType myDoFoo; - @OperationParam(name = "count") + @OperationEmbeddedParam(name = "count") private IntegerType myCount; public FooBarParams(BooleanType myDoFoo, IntegerType myCount) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index ab521d63b2c0..70bfbfc88b4c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -150,18 +150,18 @@ public MeasureReport evaluateMeasure2( public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { ourLog.info("1234: fooBar params: {}", theParams); } - - @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) - public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { - final Bundle bundle = new Bundle(); - bundle.setIdentifier(new Identifier().setValue("aValue")); - - final String bundleString = FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); - - ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); - - return bundle; - } +// +// @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) +// public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { +// final Bundle bundle = new Bundle(); +// bundle.setIdentifier(new Identifier().setValue("aValue")); +// +// final String bundleString = FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); +// +// ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); +// +// return bundle; +// } void example() { fooBar(new FooBarParams(null, null)); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java deleted file mode 100644 index 021513e14bc5..000000000000 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ReturnsBundleParams.java +++ /dev/null @@ -1,32 +0,0 @@ -package ca.uhn.fhir.cr.r4.measure; - -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; -import ca.uhn.fhir.rest.annotation.OperationParam; - -import java.util.StringJoiner; - -@OperationEmbeddedType -public class ReturnsBundleParams { - @OperationParam(name = "string") - // LUKETODO: do I always need to make it a BooleanType and not a Boolean? - private String myString; - - public ReturnsBundleParams(String myString) { - this.myString = myString; - } - - public String getString() { - return myString; - } - - public void setString(String myString) { - this.myString = myString; - } - - @Override - public String toString() { - return new StringJoiner(", ", ReturnsBundleParams.class.getSimpleName() + "[", "]") - .add("myString='" + myString + "'") - .toString(); - } -} From b1e00910c0fe9a1d9381aae2db4b8468ec41509d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 13:37:59 -0500 Subject: [PATCH 08/75] Baby steps to refactoring OperationMethodBinding to support embedded params. Spotless. --- .../server/method/OperationMethodBinding.java | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 6ee9d495a228..87d54d5db5bd 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -39,10 +39,12 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.ParametersUtil; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import java.io.IOException; import java.lang.annotation.Annotation; @@ -51,6 +53,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -63,7 +67,8 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; private final boolean myIdempotent; private final boolean myDeleteEnabled; - private final Integer myIdParamIndex; +// private final Integer myIdParamIndex; + private final OperationIdParamDetails myOperationIdParamDetails; private final String myName; private final RestOperationTypeEnum myOtherOperationType; private final ReturnTypeEnum myReturnType; @@ -172,15 +177,16 @@ protected OperationMethodBinding( myReturnType = ReturnTypeEnum.RESOURCE; } - myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); + myOperationIdParamDetails = findIdParameterDetails(theMethod); + if (getResourceName() == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; - if (myIdParamIndex != null) { + if (myOperationIdParamDetails.myIdParamIndex != null) { myCanOperateAtInstanceLevel = true; } else { myCanOperateAtServerLevel = true; } - } else if (myIdParamIndex == null) { + } else if (myOperationIdParamDetails.myIdParamIndex == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; myCanOperateAtTypeLevel = true; } else { @@ -188,7 +194,7 @@ protected OperationMethodBinding( myCanOperateAtInstanceLevel = true; // LUKETODO: Here, we need to check the operation's parameters class for the Id. - for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { + for (Annotation next : theMethod.getParameterAnnotations()[myOperationIdParamDetails.myIdParamIndex]) { // ourLog.info("1234: method: {}, param: {}, paramType: {}", theMethod.getName(), myIdParamIndex, // next.annotationType()); if (next instanceof IdParam) { @@ -398,8 +404,8 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques Msg.code(428) + message, allowedRequestTypes.toArray(RequestTypeEnum[]::new)); } - if (myIdParamIndex != null) { - theMethodParams[myIdParamIndex] = theRequest.getId(); + if (myOperationIdParamDetails.myIdParamIndex != null) { + theMethodParams[myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); } Object response = invokeServerMethod(theRequest, theMethodParams); @@ -447,6 +453,10 @@ public String getCanonicalUrl() { return myCanonicalUrl; } + private OperationIdParamDetails findIdParameterDetails(Method theMethod) { + return new OperationIdParamDetails(null, null, ParameterUtil.findIdParameterIndex(theMethod, getContext())); + } + public static class ReturnType { private int myMax; private int myMin; @@ -488,4 +498,29 @@ public void setType(String theType) { myType = theType; } } + + // LUKETODO: consider making this top-level + private static class OperationIdParamDetails { + @Nullable + private final IIdType myIdType; + @Nullable + private final IdParam myIdParam; + @Nullable + private final Integer myIdParamIndex; + + public OperationIdParamDetails(@Nullable IIdType myIdType, @Nullable IdParam myIdParam, @Nullable Integer myIdParamIndex) { + this.myIdType = myIdType; + this.myIdParam = myIdParam; + this.myIdParamIndex = myIdParamIndex; + } + + public boolean hasIdParamAnnotationAndIdTypeParamAtSameIndex() { + return false; + } + + public void assignOptionalIfIdParamPresent(Consumer theConsumer) { + Optional.ofNullable(myIdParam) + .ifPresent(nonNull -> theConsumer.accept(nonNull.optional())); + } + } } From e4141e0b6dcf96a96dc332fe6e5bd86393a23181 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 13:40:50 -0500 Subject: [PATCH 09/75] Spotless. --- .../ca/uhn/fhir/rest/param/ParameterUtil.java | 8 +- .../rest/server/method/BaseMethodBinding.java | 28 ++++--- .../fhir/rest/server/method/MethodUtil.java | 32 +++++--- .../method/OperationEmbeddedParameter.java | 4 +- .../method/OperationEmbeddedTypeUtils.java | 3 +- .../server/method/OperationMethodBinding.java | 10 ++- .../server/provider/ProviderConstants.java | 1 + .../r4/measure/CareGapsOperationProvider.java | 46 ++++++------ .../fhir/cr/r4/measure/CareGapsParams.java | 27 ++++--- .../measure/EvaluateMeasureSingleParams.java | 49 ++++++++---- .../uhn/fhir/cr/r4/measure/FooBarParams.java | 1 - .../r4/measure/MeasureOperationsProvider.java | 74 +++++++++---------- 12 files changed, 165 insertions(+), 118 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 3cf5626f2323..09c4a087261c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -155,11 +155,13 @@ public static Integer findParamAnnotationIndexFromEmbedded(Method theMethod, Cla // LUKETODO: be mindful if we get rid of OperationEmbeddedType final long operationEmbeddedTypesCount = Arrays.stream(parameterTypes) - .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) - .count(); + .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) + .count(); if (operationEmbeddedTypesCount > 1) { - throw new ConfigurationException(String.format("%sMore than one parameter with OperationEmbeddedType for method: %s", Msg.code(99999), theMethod.getName())); + throw new ConfigurationException(String.format( + "%sMore than one parameter with OperationEmbeddedType for method: %s", + Msg.code(99999), theMethod.getName())); } if (operationEmbeddedTypesCount == 0) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index ddb0cfefb889..6f0e3854286b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -262,15 +262,18 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: split this up into private methods final Class[] parameterTypes = method.getParameterTypes(); - ourLog.info("1234: invoking method for: {} and params: {} and parameterTypes: {}", method.getName(), theMethodParams, Arrays.toString(parameterTypes)); + ourLog.info( + "1234: invoking method for: {} and params: {} and parameterTypes: {}", + method.getName(), + theMethodParams, + Arrays.toString(parameterTypes)); for (Class parameterType : parameterTypes) { ourLog.info("1234: invoking parameterType: {} and method: {}", parameterType, method.getName()); final Annotation[] parameterTypeAnnotations = parameterType.getAnnotations(); final boolean hasOperationEmbeddedTypeAnnotation = - Arrays.stream(parameterTypeAnnotations) - .anyMatch(OperationEmbeddedType.class::isInstance); + Arrays.stream(parameterTypeAnnotations).anyMatch(OperationEmbeddedType.class::isInstance); if (hasOperationEmbeddedTypeAnnotation) { final Constructor[] constructors = parameterType.getConstructors(); @@ -279,7 +282,8 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! if (constructors.length > 0) { - // LUKETODO: if there are multiple constructors, cycle through them until you find one that matches the params list + // LUKETODO: if there are multiple constructors, cycle through them until you find one that + // matches the params list final Constructor constructor = constructors[0]; final Parameter[] constructorParameters = constructor.getParameters(); @@ -289,8 +293,9 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: call setters final Object operationEmbeddedType = constructor.newInstance(); final List setters = Arrays.stream(parameterType.getDeclaredMethods()) - .filter(paramMethod -> paramMethod.getName().startsWith("set")) // LUKETODO: this is nasty - .collect(Collectors.toUnmodifiableList()); + .filter(paramMethod -> + paramMethod.getName().startsWith("set")) // LUKETODO: this is nasty + .collect(Collectors.toUnmodifiableList()); for (int index = 0; index < theMethodParams.length; index++) { final Object methodParam = theMethodParams[index]; @@ -299,7 +304,10 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final Method setter = setters.get(index); final Class setterParamType = setter.getParameterTypes()[0]; - ourLog.info("1234: methodParamAtIndex: {}, setterParamType: {}", methodParamAtIndex, setterParamType); + ourLog.info( + "1234: methodParamAtIndex: {}, setterParamType: {}", + methodParamAtIndex, + setterParamType); if (methodParamAtIndex != setterParamType) { throw new RuntimeException("1234: bad params"); } @@ -319,7 +327,10 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final Class methodParamAtIndex = theMethodParams[index].getClass(); final Class parameterAtIndex = constructorParameters[index].getType(); - ourLog.info("1234: methodParamAtIndex: {}, parameterAtIndex: {}", methodParamAtIndex, parameterAtIndex); + ourLog.info( + "1234: methodParamAtIndex: {}, parameterAtIndex: {}", + methodParamAtIndex, + parameterAtIndex); if (methodParamAtIndex != parameterAtIndex) { throw new RuntimeException("1234: bad params"); } @@ -331,7 +342,6 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: design for future use factory methods return method.invoke(getProvider(), operationEmbeddedType); } - } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 1c83649b61f6..6fc36ffce504 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -116,10 +116,10 @@ public static List getResourceParameters( // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! final List> operationEmbeddedTypes = Arrays.stream(parameterTypes) - .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) - .collect(Collectors.toUnmodifiableList()); + .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) + .collect(Collectors.toUnmodifiableList()); - if (! operationEmbeddedTypes.isEmpty()) { + if (!operationEmbeddedTypes.isEmpty()) { ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); // This is the @Operation parameter on the method itself (ex: evaluateMeasure) @@ -127,8 +127,9 @@ public static List getResourceParameters( if (operationEmbeddedTypes.size() > 1) { // LUKETODO: error - throw new ConfigurationException( - String.format("%sOnly one OperationEmbeddedType is supported for now for method: %s", Msg.code(99999), theMethod.getName())); + throw new ConfigurationException(String.format( + "%sOnly one OperationEmbeddedType is supported for now for method: %s", + Msg.code(99999), theMethod.getName())); } // LUKETODO: handle multiple RequestDetails with an error @@ -148,12 +149,16 @@ public static List getResourceParameters( final Annotation[] fieldAnnotations = field.getAnnotations(); if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format("%sNo annotations for field: %s for method: %s", Msg.code(99999), fieldName, theMethod.getName())); + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(99999), fieldName, theMethod.getName())); } if (fieldAnnotations.length > 1) { // LUKETODO: error - throw new ConfigurationException(String.format("%sMore than one annotation for field: %s for method: %s", Msg.code(99999), fieldName, theMethod.getName())); + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(99999), fieldName, theMethod.getName())); } final Set annotationClassNames = Arrays.stream(fieldAnnotations) @@ -167,8 +172,8 @@ public static List getResourceParameters( fieldType.getName(), annotationClassNames); - - // This is the parameter on the field in question on the OperationEmbeddedType class: ex myCount + // This is the parameter on the field in question on the OperationEmbeddedType class: ex + // myCount final Annotation fieldAnnotation = fieldAnnotations[0]; // LUKETODO: what if this is not a IdParam or an OperationParam? @@ -193,16 +198,19 @@ public static List getResourceParameters( description, examples); - // Not sure what these are, but I think they're for params that are part of a Collection parameter + // Not sure what these are, but I think they're for params that are part of a Collection + // parameter // and may have soemthing to do with a SearchParameter final Class> outerCollectionType = null; final Class> innerCollectionType = null; - operationParameter.initializeTypes(theMethod, outerCollectionType, innerCollectionType, fieldType); + operationParameter.initializeTypes( + theMethod, outerCollectionType, innerCollectionType, fieldType); parameters.add(operationParameter); } else { - throw new ConfigurationException(Msg.code(99999) + "Unsupported param fieldType: " + fieldAnnotation); + throw new ConfigurationException( + Msg.code(99999) + "Unsupported param fieldType: " + fieldAnnotation); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index c98fac964a7d..eebd7c542b46 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -19,8 +19,8 @@ */ package ca.uhn.fhir.rest.server.method; -import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor; +import ca.uhn.fhir.context.*; import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.IQueryParameterAnd; @@ -46,8 +46,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; import java.util.function.Consumer; +import java.util.*; import static org.apache.commons.lang3.StringUtils.isNotBlank; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java index 9659f8321d9f..787a5166e0ca 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java @@ -2,5 +2,4 @@ // LUKETODO: common methods used for this stuff // LUKETODO: Spring, if applicable -public class OperationEmbeddedTypeUtils { -} +public class OperationEmbeddedTypeUtils {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 87d54d5db5bd..d45756bb4331 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -67,7 +67,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; private final boolean myIdempotent; private final boolean myDeleteEnabled; -// private final Integer myIdParamIndex; + // private final Integer myIdParamIndex; private final OperationIdParamDetails myOperationIdParamDetails; private final String myName; private final RestOperationTypeEnum myOtherOperationType; @@ -503,12 +503,15 @@ public void setType(String theType) { private static class OperationIdParamDetails { @Nullable private final IIdType myIdType; + @Nullable private final IdParam myIdParam; + @Nullable private final Integer myIdParamIndex; - public OperationIdParamDetails(@Nullable IIdType myIdType, @Nullable IdParam myIdParam, @Nullable Integer myIdParamIndex) { + public OperationIdParamDetails( + @Nullable IIdType myIdType, @Nullable IdParam myIdParam, @Nullable Integer myIdParamIndex) { this.myIdType = myIdType; this.myIdParam = myIdParam; this.myIdParamIndex = myIdParamIndex; @@ -519,8 +522,7 @@ public boolean hasIdParamAnnotationAndIdTypeParamAtSameIndex() { } public void assignOptionalIfIdParamPresent(Consumer theConsumer) { - Optional.ofNullable(myIdParam) - .ifPresent(nonNull -> theConsumer.accept(nonNull.optional())); + Optional.ofNullable(myIdParam).ifPresent(nonNull -> theConsumer.accept(nonNull.optional())); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 1b6dae48b91e..9b7d3090996b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -112,6 +112,7 @@ public class ProviderConstants { * Clinical Reasoning Operations */ public static final String CR_OPERATION_EVALUATE_MEASURE = "$evaluate-measure"; + public static final String CR_OPERATION_EVALUATE_MEASURE_2 = "$evaluate-measure2"; public static final String CR_OPERATION_CARE_GAPS = "$care-gaps"; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 5c7e4a5e8b8f..9cd3f238a76d 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -125,27 +125,27 @@ public Parameters careGapsReport( .orElse(false)); } -// @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) -// public Parameters careGapsReport2( -// // LUKETODO: include RequestDetails in Params object? -// RequestDetails theRequestDetails, -// @OperationParam(name = "params") CareGapsParams theParams) { -// -// return myR4CareGapsProcessorFactory -// .create(theRequestDetails) -// .getCareGapsReport( -// // LUKETODO: how to handle passing this down seamlessly? -// myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), -// myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), -// theParams.getSubject(), -// theParams.getStatus(), -// theParams.getMeasureId() == null -// ? null -// : theParams.getMeasureId().stream().map(IdType::new).collect(Collectors.toList()), -// theParams.getMeasureIdentifier(), -// theParams.getMeasureUrl(), -// Optional.ofNullable(theParams.getNonDocument()) -// .map(BooleanType::getValue) -// .orElse(false)); -// } + // @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) + // public Parameters careGapsReport2( + // // LUKETODO: include RequestDetails in Params object? + // RequestDetails theRequestDetails, + // @OperationParam(name = "params") CareGapsParams theParams) { + // + // return myR4CareGapsProcessorFactory + // .create(theRequestDetails) + // .getCareGapsReport( + // // LUKETODO: how to handle passing this down seamlessly? + // myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), + // myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + // theParams.getSubject(), + // theParams.getStatus(), + // theParams.getMeasureId() == null + // ? null + // : theParams.getMeasureId().stream().map(IdType::new).collect(Collectors.toList()), + // theParams.getMeasureIdentifier(), + // theParams.getMeasureUrl(), + // Optional.ofNullable(theParams.getNonDocument()) + // .map(BooleanType::getValue) + // .orElse(false)); + // } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 06333a9eee49..e0ca6f20d18f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; @@ -12,18 +12,25 @@ public class CareGapsParams { @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; + @OperationEmbeddedParam(name = "periodEnd") private final String myPeriodEnd; + @OperationEmbeddedParam(name = "subject") private final String mySubject; + @OperationEmbeddedParam(name = "status") private final List myStatus; + @OperationEmbeddedParam(name = "measureId") private final List myMeasureId; + @OperationEmbeddedParam(name = "measureIdentifier") private final List myMeasureIdentifier; + @OperationEmbeddedParam(name = "measureUrl") private final List myMeasureUrl; + @OperationEmbeddedParam(name = "nonDocument") private final BooleanType myNonDocument; @@ -81,14 +88,14 @@ public BooleanType getNonDocument() { @Override public String toString() { return new StringJoiner(", ", CareGapsParams.class.getSimpleName() + "[", "]") - .add("myPeriodStart='" + myPeriodStart + "'") - .add("myPeriodEnd='" + myPeriodEnd + "'") - .add("mySubject='" + mySubject + "'") - .add("myStatus=" + myStatus) - .add("myMeasureId=" + myMeasureId) - .add("myMeasureIdentifier=" + myMeasureIdentifier) - .add("myMeasureUrl=" + myMeasureUrl) - .add("myNonDocument=" + myNonDocument) - .toString(); + .add("myPeriodStart='" + myPeriodStart + "'") + .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("mySubject='" + mySubject + "'") + .add("myStatus=" + myStatus) + .add("myMeasureId=" + myMeasureId) + .add("myMeasureIdentifier=" + myMeasureIdentifier) + .add("myMeasureUrl=" + myMeasureUrl) + .add("myNonDocument=" + myNonDocument) + .toString(); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 78e93052a0f8..4eccb7eecfdf 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -1,8 +1,8 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; @@ -14,28 +14,49 @@ public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; + @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; + @OperationEmbeddedParam(name = "periodEnd") private final String myPeriodEnd; + @OperationEmbeddedParam(name = "reportType") private final String myReportType; + @OperationEmbeddedParam(name = "subject") private final String mySubject; + @OperationEmbeddedParam(name = "practitioner") private final String myPractitioner; + @OperationEmbeddedParam(name = "lastReceivedOn") private final String myLastReceivedOn; + @OperationEmbeddedParam(name = "productLine") private final String myProductLine; + @OperationEmbeddedParam(name = "additionalData") private final Bundle myAdditionalData; + @OperationEmbeddedParam(name = "terminologyEndpoint") private final Endpoint myTerminologyEndpoint; + @OperationEmbeddedParam(name = "parameters") private final Parameters myParameters; - public EvaluateMeasureSingleParams(IdType myId, String myPeriodStart, String myPeriodEnd, String myReportType, String mySubject, String myPractitioner, String myLastReceivedOn, String myProductLine, Bundle myAdditionalData, Endpoint myTerminologyEndpoint, Parameters myParameters) { + public EvaluateMeasureSingleParams( + IdType myId, + String myPeriodStart, + String myPeriodEnd, + String myReportType, + String mySubject, + String myPractitioner, + String myLastReceivedOn, + String myProductLine, + Bundle myAdditionalData, + Endpoint myTerminologyEndpoint, + Parameters myParameters) { this.myId = myId; this.myPeriodStart = myPeriodStart; this.myPeriodEnd = myPeriodEnd; @@ -96,17 +117,17 @@ public Parameters getParameters() { @Override public String toString() { return new StringJoiner(", ", EvaluateMeasureSingleParams.class.getSimpleName() + "[", "]") - .add("myId=" + myId) - .add("myPeriodStart='" + myPeriodStart + "'") - .add("myPeriodEnd='" + myPeriodEnd + "'") - .add("myReportType='" + myReportType + "'") - .add("mySubject='" + mySubject + "'") - .add("myPractitioner='" + myPractitioner + "'") - .add("myLastReceivedOn='" + myLastReceivedOn + "'") - .add("myProductLine='" + myProductLine + "'") - .add("myAdditionalData=" + myAdditionalData) - .add("myTerminologyEndpoint=" + myTerminologyEndpoint) - .add("myParameters=" + myParameters) - .toString(); + .add("myId=" + myId) + .add("myPeriodStart='" + myPeriodStart + "'") + .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("myReportType='" + myReportType + "'") + .add("mySubject='" + mySubject + "'") + .add("myPractitioner='" + myPractitioner + "'") + .add("myLastReceivedOn='" + myLastReceivedOn + "'") + .add("myProductLine='" + myProductLine + "'") + .add("myAdditionalData=" + myAdditionalData) + .add("myTerminologyEndpoint=" + myTerminologyEndpoint) + .add("myParameters=" + myParameters) + .toString(); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java index 07f47632d88b..91377d64699e 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; -import ca.uhn.fhir.rest.annotation.OperationParam; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IntegerType; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 70bfbfc88b4c..7cffc938b3bd 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -19,7 +19,6 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.cr.common.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; import ca.uhn.fhir.rest.annotation.IdParam; @@ -32,7 +31,6 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; @@ -111,32 +109,31 @@ public MeasureReport evaluateMeasure( } @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE_2, idempotent = true, type = Measure.class) - public MeasureReport evaluateMeasure2( - EvaluateMeasureSingleParams theParams, - RequestDetails theRequestDetails) - throws InternalErrorException, FHIRException { + public MeasureReport evaluateMeasure2(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) + throws InternalErrorException, FHIRException { // LUKETODO: Parameters within Parameters return myR4MeasureServiceFactory - .create(theRequestDetails) - .evaluate( - // LUKETODO: 1. can we support the concept of Either in hapi-fhir annotations? - // LUKETODO: 2. can we modify OperationParam to support the concept of mututally exclusive params - // LUKETODO: 3. code gen from operation definition - Eithers.forMiddle3(theParams.getId()), - // LUKETODO: push this into the hapi-fhir REST framework code - myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), - // LUKETODO: push this into the hapi-fhir REST framework code - myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), - theParams.getReportType(), - theParams.getSubject(), - theParams.getLastReceivedOn(), - null, - theParams.getTerminologyEndpoint(), - null, - theParams.getAdditionalData(), - theParams.getParameters(), - theParams.getProductLine(), - theParams.getPractitioner()); + .create(theRequestDetails) + .evaluate( + // LUKETODO: 1. can we support the concept of Either in hapi-fhir annotations? + // LUKETODO: 2. can we modify OperationParam to support the concept of mututally exclusive + // params + // LUKETODO: 3. code gen from operation definition + Eithers.forMiddle3(theParams.getId()), + // LUKETODO: push this into the hapi-fhir REST framework code + myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), + // LUKETODO: push this into the hapi-fhir REST framework code + myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + theParams.getReportType(), + theParams.getSubject(), + theParams.getLastReceivedOn(), + null, + theParams.getTerminologyEndpoint(), + null, + theParams.getAdditionalData(), + theParams.getParameters(), + theParams.getProductLine(), + theParams.getPractitioner()); } // @Operation(name = "$fooBar", manualResponse = true, idempotent = true) @@ -150,18 +147,19 @@ public MeasureReport evaluateMeasure2( public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { ourLog.info("1234: fooBar params: {}", theParams); } -// -// @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) -// public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { -// final Bundle bundle = new Bundle(); -// bundle.setIdentifier(new Identifier().setValue("aValue")); -// -// final String bundleString = FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); -// -// ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); -// -// return bundle; -// } + // + // @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) + // public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { + // final Bundle bundle = new Bundle(); + // bundle.setIdentifier(new Identifier().setValue("aValue")); + // + // final String bundleString = + // FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); + // + // ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); + // + // return bundle; + // } void example() { fooBar(new FooBarParams(null, null)); From f92abe382b6c49c293dce4a6df5ea404d5b4f9d2 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 14:17:02 -0500 Subject: [PATCH 10/75] Fix checkstyle with unique codes. --- .../uhn/fhir/rest/server/method/MethodUtil.java | 8 ++++---- .../method/OperationEmbeddedParameter.java | 15 ++++++++++----- .../server/method/OperationMethodBinding.java | 17 ++++++++++------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 6fc36ffce504..44f2df40b160 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -129,7 +129,7 @@ public static List getResourceParameters( // LUKETODO: error throw new ConfigurationException(String.format( "%sOnly one OperationEmbeddedType is supported for now for method: %s", - Msg.code(99999), theMethod.getName())); + Msg.code(9999927), theMethod.getName())); } // LUKETODO: handle multiple RequestDetails with an error @@ -151,14 +151,14 @@ public static List getResourceParameters( if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( "%sNo annotations for field: %s for method: %s", - Msg.code(99999), fieldName, theMethod.getName())); + Msg.code(9999926), fieldName, theMethod.getName())); } if (fieldAnnotations.length > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sMore than one annotation for field: %s for method: %s", - Msg.code(99999), fieldName, theMethod.getName())); + Msg.code(999998), fieldName, theMethod.getName())); } final Set annotationClassNames = Arrays.stream(fieldAnnotations) @@ -210,7 +210,7 @@ public static List getResourceParameters( parameters.add(operationParameter); } else { throw new ConfigurationException( - Msg.code(99999) + "Unsupported param fieldType: " + fieldAnnotation); + Msg.code(999995) + "Unsupported param fieldType: " + fieldAnnotation); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index eebd7c542b46..3045a99d6438 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -221,7 +221,8 @@ public void initializeTypes( myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); myConverter = new OperationParamConverter(); } else { - throw new ConfigurationException(Msg.code(361) + "Invalid type for @OperationParam on method " + // LUKETODO: claim new code + throw new ConfigurationException(Msg.code(999991) + "Invalid type for @OperationParam on method " + theMethod + ": " + myParameterType.getName()); } } @@ -239,7 +240,8 @@ public static void validateTypeIsAppropriateVersionForContext( FhirVersionEnum elementVersion = FhirVersionEnum.determineVersionForType(theParameterType); if (elementVersion != null) { if (elementVersion != theContext.getVersion().getVersion()) { - throw new ConfigurationException(Msg.code(360) + "Incorrect use of type " + // LUKETODO: claim new code + throw new ConfigurationException(Msg.code(9999992) + "Incorrect use of type " + theParameterType.getSimpleName() + " as " + theUseDescription + " type for method when theContext is for version " + theContext.getVersion().getVersion().name() + " in method: " + theMethod.toString()); @@ -254,7 +256,8 @@ OperationEmbeddedParameter setConverter(IOperationParamConverter theConverter) { } private void throwWrongParamType(Object nextValue) { - throw new InvalidRequestException(Msg.code(362) + "Request has parameter " + myName + " of type " + // LUKETODO: claim new code + throw new InvalidRequestException(Msg.code(9999993) + "Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); } @@ -385,7 +388,8 @@ private void translateQueryParametersIntoServerArgumentForGet( theRequest.getServer().getFhirContext().getLocalizer(); String msg = localizer.getMessage( OperationEmbeddedParameter.class, "urlParamNotPrimitive", myOperationName, myName); - throw new MethodNotAllowedException(Msg.code(363) + msg, RequestTypeEnum.POST); + // LUKETODO: claim new code + throw new MethodNotAllowedException(Msg.code(99999993) + msg, RequestTypeEnum.POST); } } } @@ -537,6 +541,7 @@ public Object outgoingClient(Object theObject) { } public static void throwInvalidMode(String paramValues) { - throw new InvalidRequestException(Msg.code(364) + "Invalid mode value: \"" + paramValues + "\""); + // LUKETODO: claim new code + throw new InvalidRequestException(Msg.code(99999994) + "Invalid mode value: \"" + paramValues + "\""); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index d45756bb4331..8dc11e62dab5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -67,7 +67,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; private final boolean myIdempotent; private final boolean myDeleteEnabled; - // private final Integer myIdParamIndex; private final OperationIdParamDetails myOperationIdParamDetails; private final String myName; private final RestOperationTypeEnum myOtherOperationType; @@ -404,17 +403,21 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques Msg.code(428) + message, allowedRequestTypes.toArray(RequestTypeEnum[]::new)); } - if (myOperationIdParamDetails.myIdParamIndex != null) { - theMethodParams[myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); - } + final Object response = invokeEitherParamsOrEmbeddedParams(theRequest, theMethodParams); - Object response = invokeServerMethod(theRequest, theMethodParams); if (myManualResponseMode) { return null; } - IBundleProvider retVal = toResourceList(response); - return retVal; + return toResourceList(response); + } + + private Object invokeEitherParamsOrEmbeddedParams(RequestDetails theRequest, Object[] theMethodParams) { + if (myOperationIdParamDetails.myIdParamIndex != null) { + theMethodParams[myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); + } + + return invokeServerMethod(theRequest, theMethodParams); } public boolean isCanOperateAtInstanceLevel() { From 78711a29ebb45bc72eaf693a4de5049a16b78dc3 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 15:14:10 -0500 Subject: [PATCH 11/75] Fix checkstyle. --- .../rest/server/method/BaseMethodBinding.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 6f0e3854286b..3d0a58ba358b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -128,6 +128,19 @@ protected Object[] createMethodParams(RequestDetails theRequest) { return params; } + // LUKETODO: JP deleted this for some reason: why? + protected Object[] createParametersForServerRequest(RequestDetails theRequest) { + Object[] params = new Object[getParameters().size()]; + for (int i = 0; i < getParameters().size(); i++) { + IParameter param = getParameters().get(i); + if (param == null) { + continue; + } + params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); + } + return params; + } + /** * Subclasses may override to declare that they apply to all resource types */ @@ -309,7 +322,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th methodParamAtIndex, setterParamType); if (methodParamAtIndex != setterParamType) { - throw new RuntimeException("1234: bad params"); + throw new RuntimeException(Msg.code(12345523) + "1234: bad params"); } setter.invoke(methodParam); @@ -320,7 +333,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th } else { // LUKETODO: else? if (theMethodParams.length != constructorParameters.length) { - throw new RuntimeException("1234: bad params"); + throw new RuntimeException(Msg.code(234198921) + "1234: bad params"); } for (int index = 0; index < theMethodParams.length; index++) { @@ -332,7 +345,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th methodParamAtIndex, parameterAtIndex); if (methodParamAtIndex != parameterAtIndex) { - throw new RuntimeException("1234: bad params"); + throw new RuntimeException(Msg.code(236146124) + "1234: bad params"); } } From a0d697ec295c9028d62c41a5be03b58933fcf5b7 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Jan 2025 16:35:15 -0500 Subject: [PATCH 12/75] Clone of $evaluate-measure works but in a really messy way. --- .../rest/server/method/BaseMethodBinding.java | 91 ++++++----- .../server/method/OperationMethodBinding.java | 141 ++++++++++++++---- 2 files changed, 168 insertions(+), 64 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 3d0a58ba358b..72e7106589ad 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -46,6 +46,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.BundleProviders; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -67,10 +68,13 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import static java.util.function.Predicate.not; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseMethodBinding { @@ -303,56 +307,65 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: mandate an immutable class with a constructor to set params if (constructorParameters.length == 0) { - // LUKETODO: call setters - final Object operationEmbeddedType = constructor.newInstance(); - final List setters = Arrays.stream(parameterType.getDeclaredMethods()) - .filter(paramMethod -> - paramMethod.getName().startsWith("set")) // LUKETODO: this is nasty - .collect(Collectors.toUnmodifiableList()); - - for (int index = 0; index < theMethodParams.length; index++) { - final Object methodParam = theMethodParams[index]; - final Class methodParamAtIndex = methodParam.getClass(); - // LUKETODO: check at least one param - final Method setter = setters.get(index); - final Class setterParamType = setter.getParameterTypes()[0]; + throw new InternalErrorException( + Msg.code(234198927) + "No constructor that takes parameters!!!"); + } else { + final Object[] methodParamsWithoutRequestDetails = removeRequestDetails(theMethodParams); + // LUKETODO: else? + if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { + // LUKETODO: we blow up here because RequestDetails is the EXTRA PARAMETER! + throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); + } - ourLog.info( - "1234: methodParamAtIndex: {}, setterParamType: {}", - methodParamAtIndex, - setterParamType); - if (methodParamAtIndex != setterParamType) { - throw new RuntimeException(Msg.code(12345523) + "1234: bad params"); - } + final int[] requestDetailsIndexes = IntStream.range(0, theMethodParams.length) + .filter(index -> theMethodParams[index] instanceof RequestDetails) + .toArray(); - setter.invoke(methodParam); + if (requestDetailsIndexes.length > 1) { + throw new InternalErrorException(Msg.code(562462) + + "1234: cannot define a request with more than one RequestDetails"); } - ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); - return method.invoke(getProvider(), operationEmbeddedType); - } else { - // LUKETODO: else? - if (theMethodParams.length != constructorParameters.length) { - throw new RuntimeException(Msg.code(234198921) + "1234: bad params"); - } + final Optional optArgPositionRequestDetails = requestDetailsIndexes.length > 0 + ? Optional.of(requestDetailsIndexes[0]) + : Optional.empty(); - for (int index = 0; index < theMethodParams.length; index++) { - final Class methodParamAtIndex = theMethodParams[index].getClass(); - final Class parameterAtIndex = constructorParameters[index].getType(); + for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { + final Object methodParamAtIndex = methodParamsWithoutRequestDetails[index]; + if (methodParamAtIndex == null) { + // argument is null, so we can't the type, so skip it: + continue; + } + final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); + final Class parameterClassAtIndex = constructorParameters[index].getType(); ourLog.info( - "1234: methodParamAtIndex: {}, parameterAtIndex: {}", - methodParamAtIndex, - parameterAtIndex); - if (methodParamAtIndex != parameterAtIndex) { + "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", + methodParamClassAtIndex, + parameterClassAtIndex); + + if (methodParamClassAtIndex != parameterClassAtIndex) { throw new RuntimeException(Msg.code(236146124) + "1234: bad params"); } } - final Object operationEmbeddedType = constructor.newInstance(theMethodParams); + final Object operationEmbeddedType = + constructor.newInstance(methodParamsWithoutRequestDetails); ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); // LUKETODO: design for future use factory methods + // LUKETODO: how do I know where to put the RequestDetails???? + if (optArgPositionRequestDetails.isPresent()) { + final Integer requestDetailsIndex = optArgPositionRequestDetails.get(); + + // LUKETODO: review this: this is tacky: + if (requestDetailsIndex == 0) { + return method.invoke(getProvider(), theMethodParams[0], operationEmbeddedType); + } + + return method.invoke( + getProvider(), operationEmbeddedType, theMethodParams[requestDetailsIndex]); + } return method.invoke(getProvider(), operationEmbeddedType); } } @@ -374,6 +387,12 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th } } + private static Object[] removeRequestDetails(Object[] theMethodParams) { + return Arrays.stream(theMethodParams) + .filter(not(RequestDetails.class::isInstance).and(not(SystemRequestDetails.class::isInstance))) + .toArray(); + } + /** * Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally. */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 8dc11e62dab5..85c70596d78a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.RequiredParam; @@ -37,6 +38,7 @@ import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -48,13 +50,14 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Consumer; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -150,7 +153,7 @@ protected OperationMethodBinding( + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() + " but this annotation has no name defined"); } - if (theOperationName.startsWith("$") == false) { + if (!theOperationName.startsWith("$")) { theOperationName = "$" + theOperationName; } myName = theOperationName; @@ -158,7 +161,7 @@ protected OperationMethodBinding( try { if (theReturnTypeFromRp != null) { setResourceName(theContext.getResourceType(theReturnTypeFromRp)); - } else if (theOperationType != null && Modifier.isAbstract(theOperationType.getModifiers()) == false) { + } else if (theOperationType != null && !Modifier.isAbstract(theOperationType.getModifiers())) { setResourceName(theContext.getResourceType(theOperationType)); } else if (isNotBlank(theOperationTypeName)) { setResourceName(theContext.getResourceType(theOperationTypeName)); @@ -176,7 +179,7 @@ protected OperationMethodBinding( myReturnType = ReturnTypeEnum.RESOURCE; } - myOperationIdParamDetails = findIdParameterDetails(theMethod); + myOperationIdParamDetails = findIdParameterDetails(theMethod, getContext()); if (getResourceName() == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; @@ -191,15 +194,7 @@ protected OperationMethodBinding( } else { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; myCanOperateAtInstanceLevel = true; - - // LUKETODO: Here, we need to check the operation's parameters class for the Id. - for (Annotation next : theMethod.getParameterAnnotations()[myOperationIdParamDetails.myIdParamIndex]) { - // ourLog.info("1234: method: {}, param: {}, paramType: {}", theMethod.getName(), myIdParamIndex, - // next.annotationType()); - if (next instanceof IdParam) { - myCanOperateAtTypeLevel = ((IdParam) next).optional(); - } - } + myCanOperateAtServerLevel = myOperationIdParamDetails.setOrReturnPreviousValue(myCanOperateAtServerLevel); } myReturnParams = new ArrayList<>(); @@ -314,6 +309,7 @@ public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequ boolean requestHasId = theRequest.getId() != null; if (requestHasId) { + ourLog.info("1234: method: {} has myCanOperateAtInstanceLevel : {}", myName, myCanOperateAtInstanceLevel); return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; } if (isNotBlank(theRequest.getResourceName())) { @@ -403,7 +399,8 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques Msg.code(428) + message, allowedRequestTypes.toArray(RequestTypeEnum[]::new)); } - final Object response = invokeEitherParamsOrEmbeddedParams(theRequest, theMethodParams); + final Object response = + invokeEitherParamsOrEmbeddedParams(theRequest, theMethodParams, myOperationIdParamDetails); if (myManualResponseMode) { return null; @@ -412,9 +409,12 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques return toResourceList(response); } - private Object invokeEitherParamsOrEmbeddedParams(RequestDetails theRequest, Object[] theMethodParams) { - if (myOperationIdParamDetails.myIdParamIndex != null) { - theMethodParams[myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); + private Object invokeEitherParamsOrEmbeddedParams( + RequestDetails theRequest, Object[] theMethodParams, OperationIdParamDetails theOperationIdParamDetails) { + ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); + + if (this.myOperationIdParamDetails.myIdParamIndex != null) { + theMethodParams[this.myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); } return invokeServerMethod(theRequest, theMethodParams); @@ -456,8 +456,87 @@ public String getCanonicalUrl() { return myCanonicalUrl; } - private OperationIdParamDetails findIdParameterDetails(Method theMethod) { - return new OperationIdParamDetails(null, null, ParameterUtil.findIdParameterIndex(theMethod, getContext())); + private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { + final Class[] parameterTypes = theMethod.getParameterTypes(); + + final List> operationEmbeddedTypes = Arrays.stream(parameterTypes) + .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) + .collect(Collectors.toUnmodifiableList()); + + if (!operationEmbeddedTypes.isEmpty()) { + return findIdParamIndexForOperationEmbeddedType(theMethod, operationEmbeddedTypes, theContext); + } + + return getIdParamAnnotationFromMethodParams(theMethod, theContext); + } + + @Nonnull + private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theMethod, FhirContext theContext) { + final Integer paramAnnotationIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); + + if (paramAnnotationIndex != null) { + final Class paramType = theMethod.getParameterTypes()[paramAnnotationIndex]; + if (IIdType.class.equals(paramType)) { + final Annotation[] parameterAnnotation = theMethod.getParameterAnnotations()[paramAnnotationIndex]; + + for (Annotation nextParameterAnnotation : parameterAnnotation) { + if (nextParameterAnnotation instanceof IdParam) { + return new OperationIdParamDetails( + null, (IdParam) nextParameterAnnotation, paramAnnotationIndex, theMethod); + } + } + return new OperationIdParamDetails(null, null, paramAnnotationIndex, theMethod); + } + } + + return new OperationIdParamDetails(null, null, paramAnnotationIndex, theMethod); + } + + @Nonnull + private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( + Method theMethod, List> theOperationEmbeddedTypes, FhirContext theContext) { + for (Class operationEmbeddedTypes : theOperationEmbeddedTypes) { + if (operationEmbeddedTypes.equals(RequestDetails.class) + || operationEmbeddedTypes.equals(ServletRequestDetails.class)) { + // skip + } else { + final Field[] fields = operationEmbeddedTypes.getDeclaredFields(); + + int paramIndex = 0; + for (Field field : fields) { + final String fieldName = field.getName(); + final Annotation[] fieldAnnotations = field.getAnnotations(); + + if (fieldAnnotations.length < 1) { + // LUKETODO: which Exception should we throw here? + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(126362643), fieldName, theMethod.getName())); + } + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + // LUKETODO: which Exception should we throw here? + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(195614846), fieldName, theMethod.getName())); + } + + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + return new OperationIdParamDetails(null, (IdParam) fieldAnnotation, paramIndex, theMethod); + } + + final boolean isRi = theContext.getVersion().getVersion().isRi(); + // LUKETODO: usesHapiId + + paramIndex++; + } + } + } + + return new OperationIdParamDetails(null, null, null, theMethod); } public static class ReturnType { @@ -510,22 +589,28 @@ private static class OperationIdParamDetails { @Nullable private final IdParam myIdParam; + // LUKETODO: can this ever be null? @Nullable private final Integer myIdParamIndex; + private final Method myMethod; + + // LUKETODO: add a NOTHING factory method public OperationIdParamDetails( - @Nullable IIdType myIdType, @Nullable IdParam myIdParam, @Nullable Integer myIdParamIndex) { - this.myIdType = myIdType; - this.myIdParam = myIdParam; - this.myIdParamIndex = myIdParamIndex; + @Nullable IIdType theIdType, + @Nullable IdParam theIdParam, + @Nullable Integer theIdParamIndex, + Method theMethod) { + myIdType = theIdType; + myIdParam = theIdParam; + myIdParamIndex = theIdParamIndex; + myMethod = theMethod; } - public boolean hasIdParamAnnotationAndIdTypeParamAtSameIndex() { - return false; - } + public void assigneMethodParamsIfApplicable() {} - public void assignOptionalIfIdParamPresent(Consumer theConsumer) { - Optional.ofNullable(myIdParam).ifPresent(nonNull -> theConsumer.accept(nonNull.optional())); + public boolean setOrReturnPreviousValue(boolean thePreviousValue) { + return Optional.ofNullable(myIdParam).map(IdParam::optional).orElse(thePreviousValue); } } } From 5804f1660fbf4349790a66d44565c0dc73169c5a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 10:17:12 -0500 Subject: [PATCH 13/75] Clone of $care-gaps works but in a really messy way. Revert changes to OperationParameter. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 1 + .../rest/server/method/BaseMethodBinding.java | 168 ++++++++++-------- .../fhir/rest/server/method/MethodUtil.java | 83 ++++++++- .../server/method/OperationEmbeddedUtils.java | 4 + .../server/method/OperationMethodBinding.java | 1 + .../server/method/OperationParameter.java | 27 +-- .../r4/measure/CareGapsOperationProvider.java | 47 ++--- 7 files changed, 211 insertions(+), 120 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 0b0b89f13132..1c4847e10705 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; +// LUKETODO: consider enhancing this class with operation params stuff instead public class ReflectionUtil { public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 72e7106589ad..4e752a8a010f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -285,88 +285,110 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th theMethodParams, Arrays.toString(parameterTypes)); - for (Class parameterType : parameterTypes) { - ourLog.info("1234: invoking parameterType: {} and method: {}", parameterType, method.getName()); - final Annotation[] parameterTypeAnnotations = parameterType.getAnnotations(); - - final boolean hasOperationEmbeddedTypeAnnotation = - Arrays.stream(parameterTypeAnnotations).anyMatch(OperationEmbeddedType.class::isInstance); - - if (hasOperationEmbeddedTypeAnnotation) { - final Constructor[] constructors = parameterType.getConstructors(); - - // LUKETODO: what if there's a noarg constructor and all we have is setters? - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - if (constructors.length > 0) { - // LUKETODO: if there are multiple constructors, cycle through them until you find one that - // matches the params list - final Constructor constructor = constructors[0]; - - final Parameter[] constructorParameters = constructor.getParameters(); - - // LUKETODO: mandate an immutable class with a constructor to set params - if (constructorParameters.length == 0) { - throw new InternalErrorException( - Msg.code(234198927) + "No constructor that takes parameters!!!"); - } else { - final Object[] methodParamsWithoutRequestDetails = removeRequestDetails(theMethodParams); - // LUKETODO: else? - if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { - // LUKETODO: we blow up here because RequestDetails is the EXTRA PARAMETER! - throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); - } - - final int[] requestDetailsIndexes = IntStream.range(0, theMethodParams.length) - .filter(index -> theMethodParams[index] instanceof RequestDetails) - .toArray(); - - if (requestDetailsIndexes.length > 1) { - throw new InternalErrorException(Msg.code(562462) - + "1234: cannot define a request with more than one RequestDetails"); - } - - final Optional optArgPositionRequestDetails = requestDetailsIndexes.length > 0 - ? Optional.of(requestDetailsIndexes[0]) - : Optional.empty(); - - for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { - final Object methodParamAtIndex = methodParamsWithoutRequestDetails[index]; - if (methodParamAtIndex == null) { - // argument is null, so we can't the type, so skip it: - continue; + if (Arrays.stream(parameterTypes) + .map(Class::getAnnotations) + .map(Arrays::asList) + .flatMap(Collection::stream) + .anyMatch(OperationEmbeddedType.class::isInstance)) { + + for (Class parameterType : parameterTypes) { + ourLog.info("1234: invoking parameterType: {} and method: {}", parameterType, method.getName()); + final Annotation[] parameterTypeAnnotations = parameterType.getAnnotations(); + + final boolean hasOperationEmbeddedTypeAnnotation = + Arrays.stream(parameterTypeAnnotations).anyMatch(OperationEmbeddedType.class::isInstance); + + if (hasOperationEmbeddedTypeAnnotation) { + final Constructor[] constructors = parameterType.getConstructors(); + + // LUKETODO: what if there's a noarg constructor and all we have is setters? + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + if (constructors.length > 0) { + // LUKETODO: if there are multiple constructors, cycle through them until you find one that + // matches the params list + final Constructor constructor = constructors[0]; + + final Parameter[] constructorParameters = constructor.getParameters(); + + // LUKETODO: mandate an immutable class with a constructor to set params + if (constructorParameters.length == 0) { + throw new InternalErrorException( + Msg.code(234198927) + "No constructor that takes parameters!!!"); + } else { + final Object[] methodParamsWithoutRequestDetails = + removeRequestDetails(theMethodParams); + // LUKETODO: else? + if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { + // LUKETODO: we blow up here because RequestDetails is the EXTRA PARAMETER! + throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); } - final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); - final Class parameterClassAtIndex = constructorParameters[index].getType(); - ourLog.info( - "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", - methodParamClassAtIndex, - parameterClassAtIndex); + final int[] requestDetailsIndexes = IntStream.range(0, theMethodParams.length) + .filter(index -> theMethodParams[index] instanceof RequestDetails) + .toArray(); - if (methodParamClassAtIndex != parameterClassAtIndex) { - throw new RuntimeException(Msg.code(236146124) + "1234: bad params"); + if (requestDetailsIndexes.length > 1) { + throw new InternalErrorException(Msg.code(562462) + + "1234: cannot define a request with more than one RequestDetails"); } - } - final Object operationEmbeddedType = - constructor.newInstance(methodParamsWithoutRequestDetails); + final Optional optArgPositionRequestDetails = requestDetailsIndexes.length > 0 + ? Optional.of(requestDetailsIndexes[0]) + : Optional.empty(); + + for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { + final Object methodParamAtIndex = methodParamsWithoutRequestDetails[index]; + if (methodParamAtIndex == null) { + // argument is null, so we can't the type, so skip it: + continue; + } + final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); + final Class parameterClassAtIndex = constructorParameters[index].getType(); + + ourLog.info( + "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", + methodParamClassAtIndex, + parameterClassAtIndex); + + // LUKETODO: fix this this is gross + if (Collection.class.isAssignableFrom(methodParamClassAtIndex) + || Collection.class.isAssignableFrom(parameterClassAtIndex)) { + // ex: List and ArrayList + if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { + throw new InternalErrorException(String.format( + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146124), + methodParamClassAtIndex, + parameterClassAtIndex)); + } + } else if (methodParamClassAtIndex != parameterClassAtIndex) { + throw new InternalErrorException(String.format( + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); + } + } - ourLog.info("1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); - // LUKETODO: design for future use factory methods - // LUKETODO: how do I know where to put the RequestDetails???? - if (optArgPositionRequestDetails.isPresent()) { - final Integer requestDetailsIndex = optArgPositionRequestDetails.get(); + final Object operationEmbeddedType = + constructor.newInstance(methodParamsWithoutRequestDetails); - // LUKETODO: review this: this is tacky: - if (requestDetailsIndex == 0) { - return method.invoke(getProvider(), theMethodParams[0], operationEmbeddedType); + ourLog.info( + "1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); + // LUKETODO: design for future use factory methods + // LUKETODO: how do I know where to put the RequestDetails???? + if (optArgPositionRequestDetails.isPresent()) { + final Integer requestDetailsIndex = optArgPositionRequestDetails.get(); + + // LUKETODO: review this: this is tacky: + if (requestDetailsIndex == 0) { + return method.invoke(getProvider(), theMethodParams[0], operationEmbeddedType); + } + + return method.invoke( + getProvider(), operationEmbeddedType, theMethodParams[requestDetailsIndex]); } - - return method.invoke( - getProvider(), operationEmbeddedType, theMethodParams[requestDetailsIndex]); + return method.invoke(getProvider(), operationEmbeddedType); } - return method.invoke(getProvider(), operationEmbeddedType); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 44f2df40b160..7b7668469fcf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -198,14 +198,78 @@ public static List getResourceParameters( description, examples); - // Not sure what these are, but I think they're for params that are part of a Collection - // parameter - // and may have soemthing to do with a SearchParameter - final Class> outerCollectionType = null; - final Class> innerCollectionType = null; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + parameterType = fieldType; + + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + if (parameterType == null + && theMethod.getDeclaringClass().isSynthetic()) { + try { + theMethod = theMethod + .getDeclaringClass() + .getSuperclass() + .getMethod(theMethod.getName(), parameterTypes); + parameterType = + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter( + theMethod, paramIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + theMethod.getName() + "' does not exist for super class '" + + theMethod.getDeclaringClass().getSuperclass() + "'"); + } + } + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: now we're processing the generic parameter, so capture the inner and outer + // types + // Collection + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: as a guard: if this is still a Collection, then throw because something went + // wrong + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException( + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName() + + "' in type '" + + theMethod.getDeclaringClass().getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + // LUKETODO: do I need to worry about this: + /* + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + theMethod); + } + parameterType = newParameterType; + */ + + ourLog.info( + "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", + theMethod.getName(), + outerCollectionType, + innerCollectionType, + parameterType); operationParameter.initializeTypes( - theMethod, outerCollectionType, innerCollectionType, fieldType); + theMethod, outerCollectionType, innerCollectionType, parameterType); parameters.add(operationParameter); } else { @@ -535,6 +599,13 @@ public Object outgoingClient(Object theObject) { // LUKETODO: Or do we expand the paramters here, and then foreaach parameters.add() ??? // ourLog.info("1234: param class: {}, method: {}", param.getClass().getCanonicalName(), // theMethod.getName()); + + ourLog.info( + "1234:about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", + theMethod.getName(), + outerCollectionType, + innerCollectionType, + parameterType); param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); parameters.add(param); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java new file mode 100644 index 000000000000..32e5534bbd7e --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java @@ -0,0 +1,4 @@ +package ca.uhn.fhir.rest.server.method; + +// LUKETODO: find good reusable patterns and maintain them here +public class OperationEmbeddedUtils {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 85c70596d78a..30fb34d4f1b8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -413,6 +413,7 @@ private Object invokeEitherParamsOrEmbeddedParams( RequestDetails theRequest, Object[] theMethodParams, OperationIdParamDetails theOperationIdParamDetails) { ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); + // LUKETODO: this is gross: fix this: if (this.myOperationIdParamDetails.myIdParamIndex != null) { theMethodParams[this.myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index da8d85571ffb..13f11945e3fe 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -35,7 +35,6 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -69,13 +68,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; -// LUKETODO: DO NOT HIJACK THIS -// LUKETODO: clone this and use this for Embedded object params -// LUKETODO: like EmbeddedOperationParameter public class OperationParameter implements IParameter { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationParameter.class); - static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") @@ -91,12 +85,12 @@ public class OperationParameter implements IParameter { private Class myInnerCollectionType; private int myMax; - private final int myMin; + private int myMin; private Class myParameterType; private String myParamType; private SearchParameter mySearchParameterBinding; - private final String myDescription; - private final List myExampleValues; + private String myDescription; + private List myExampleValues; OperationParameter( FhirContext theCtx, @@ -123,7 +117,7 @@ public class OperationParameter implements IParameter { @SuppressWarnings({"rawtypes", "unchecked"}) private void addValueToList(List matchingParamValues, Object values) { if (values != null) { - if (BaseAndListParam.class.isAssignableFrom(myParameterType) && !matchingParamValues.isEmpty()) { + if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); BaseAndListParam newAndList = (BaseAndListParam) values; for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { @@ -206,14 +200,11 @@ public void initializeTypes( || isSearchParam || ValidationModeEnum.class.equals(myParameterType); - final boolean isAnnotationPresent = myParameterType.isAnnotationPresent(OperationEmbeddedType.class); - /* * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We * should probably clean this up.. */ if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { - // LUKETODO: this is where we get the Exception: add an else if if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { @@ -267,7 +258,7 @@ public static void validateTypeIsAppropriateVersionForContext( } } - OperationParameter setConverter(IOperationParamConverter theConverter) { + public OperationParameter setConverter(IOperationParamConverter theConverter) { myConverter = theConverter; return this; } @@ -311,7 +302,7 @@ private void translateQueryParametersIntoServerArgumentForGet( RequestDetails theRequest, List matchingParamValues) { if (mySearchParameterBinding != null) { - List params = new ArrayList<>(); + List params = new ArrayList(); String nameWithQualifierColon = myName + ":"; for (String nextParamName : theRequest.getParameters().keySet()) { @@ -447,7 +438,7 @@ private void translateQueryParametersIntoServerArgumentForPost( List values = paramChildAccessor.getValues(requestContents); for (IBase nextParameter : values) { List nextNames = nameChild.getAccessor().getValues(nextParameter); - if (nextNames != null && !nextNames.isEmpty()) { + if (nextNames != null && nextNames.size() > 0) { IPrimitiveType nextName = (IPrimitiveType) nextNames.get(0); if (myName.equals(nextName.getValueAsString())) { @@ -458,9 +449,9 @@ private void translateQueryParametersIntoServerArgumentForPost( valueChild.getAccessor().getValues(nextParameter); List paramResources = resourceChild.getAccessor().getValues(nextParameter); - if (paramValues != null && !paramValues.isEmpty()) { + if (paramValues != null && paramValues.size() > 0) { tryToAddValues(paramValues, matchingParamValues); - } else if (paramResources != null && !paramResources.isEmpty()) { + } else if (paramResources != null && paramResources.size() > 0) { tryToAddValues(paramResources, matchingParamValues); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 9cd3f238a76d..f3654da33a2a 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -125,27 +125,28 @@ public Parameters careGapsReport( .orElse(false)); } - // @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) - // public Parameters careGapsReport2( - // // LUKETODO: include RequestDetails in Params object? - // RequestDetails theRequestDetails, - // @OperationParam(name = "params") CareGapsParams theParams) { - // - // return myR4CareGapsProcessorFactory - // .create(theRequestDetails) - // .getCareGapsReport( - // // LUKETODO: how to handle passing this down seamlessly? - // myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), - // myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), - // theParams.getSubject(), - // theParams.getStatus(), - // theParams.getMeasureId() == null - // ? null - // : theParams.getMeasureId().stream().map(IdType::new).collect(Collectors.toList()), - // theParams.getMeasureIdentifier(), - // theParams.getMeasureUrl(), - // Optional.ofNullable(theParams.getNonDocument()) - // .map(BooleanType::getValue) - // .orElse(false)); - // } + @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) + public Parameters careGapsReport2( + // LUKETODO: include RequestDetails in Params object? + RequestDetails theRequestDetails, @OperationParam(name = "params") CareGapsParams theParams) { + + return myR4CareGapsProcessorFactory + .create(theRequestDetails) + .getCareGapsReport( + // LUKETODO: how to handle passing this down seamlessly? + myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), + myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + theParams.getSubject(), + theParams.getStatus(), + theParams.getMeasureId() == null + ? null + : theParams.getMeasureId().stream() + .map(IdType::new) + .collect(Collectors.toList()), + theParams.getMeasureIdentifier(), + theParams.getMeasureUrl(), + Optional.ofNullable(theParams.getNonDocument()) + .map(BooleanType::getValue) + .orElse(false)); + } } From 3d1dcc181bccffd71912e01464f5f1b9c8278ff2 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 11:41:52 -0500 Subject: [PATCH 14/75] Eliminate duplicate methods and migrate the main methods to operation params. --- .../server/provider/ProviderConstants.java | 3 -- .../r4/measure/CareGapsOperationProvider.java | 30 +-------------- .../r4/measure/MeasureOperationsProvider.java | 37 +------------------ 3 files changed, 3 insertions(+), 67 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 9b7d3090996b..258fffae774b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -113,10 +113,7 @@ public class ProviderConstants { */ public static final String CR_OPERATION_EVALUATE_MEASURE = "$evaluate-measure"; - public static final String CR_OPERATION_EVALUATE_MEASURE_2 = "$evaluate-measure2"; - public static final String CR_OPERATION_CARE_GAPS = "$care-gaps"; - public static final String CR_OPERATION_CARE_GAPS_2 = "$care-gaps2"; public static final String CR_OPERATION_SUBMIT_DATA = "$submit-data"; public static final String CR_OPERATION_EVALUATE = "$evaluate"; public static final String CR_OPERATION_CQL = "$cql"; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index f3654da33a2a..e7db933df1f5 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -50,6 +50,7 @@ public CareGapsOperationProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } + // LUKETODO: fix javadoc /** * Implements the $care-gaps @@ -98,35 +99,6 @@ public CareGapsOperationProvider( "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) public Parameters careGapsReport( - RequestDetails theRequestDetails, - @OperationParam(name = "periodStart") String thePeriodStart, - @OperationParam(name = "periodEnd") String thePeriodEnd, - @OperationParam(name = "subject") String theSubject, - @OperationParam(name = "status") List theStatus, - @OperationParam(name = "measureId") List theMeasureId, - @OperationParam(name = "measureIdentifier") List theMeasureIdentifier, - @OperationParam(name = "measureUrl") List theMeasureUrl, - @OperationParam(name = "nonDocument") BooleanType theNonDocument) { - - return myR4CareGapsProcessorFactory - .create(theRequestDetails) - .getCareGapsReport( - myStringTimePeriodHandler.getStartZonedDateTime(thePeriodStart, theRequestDetails), - myStringTimePeriodHandler.getEndZonedDateTime(thePeriodEnd, theRequestDetails), - theSubject, - theStatus, - theMeasureId == null - ? null - : theMeasureId.stream().map(IdType::new).collect(Collectors.toList()), - theMeasureIdentifier, - theMeasureUrl, - Optional.ofNullable(theNonDocument) - .map(BooleanType::getValue) - .orElse(false)); - } - - @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS_2, idempotent = true, type = Measure.class) - public Parameters careGapsReport2( // LUKETODO: include RequestDetails in Params object? RequestDetails theRequestDetails, @OperationParam(name = "params") CareGapsParams theParams) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 7cffc938b3bd..f4d28ca31a66 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -51,6 +51,7 @@ public MeasureOperationsProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } + // LUKETODO: fix javadoc /** * Implements the $evaluate-measure @@ -75,41 +76,7 @@ public MeasureOperationsProvider( * @return the calculated MeasureReport */ @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE, idempotent = true, type = Measure.class) - public MeasureReport evaluateMeasure( - @IdParam IdType theId, - @OperationParam(name = "periodStart") String thePeriodStart, - @OperationParam(name = "periodEnd") String thePeriodEnd, - @OperationParam(name = "reportType") String theReportType, - @OperationParam(name = "subject") String theSubject, - @OperationParam(name = "practitioner") String thePractitioner, - @OperationParam(name = "lastReceivedOn") String theLastReceivedOn, - @OperationParam(name = "productLine") String theProductLine, - @OperationParam(name = "additionalData") Bundle theAdditionalData, - @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, - @OperationParam(name = "parameters") Parameters theParameters, - RequestDetails theRequestDetails) - throws InternalErrorException, FHIRException { - // LUKETODO: Parameters within Parameters - return myR4MeasureServiceFactory - .create(theRequestDetails) - .evaluate( - Eithers.forMiddle3(theId), - myStringTimePeriodHandler.getStartZonedDateTime(thePeriodStart, theRequestDetails), - myStringTimePeriodHandler.getEndZonedDateTime(thePeriodEnd, theRequestDetails), - theReportType, - theSubject, - theLastReceivedOn, - null, - theTerminologyEndpoint, - null, - theAdditionalData, - theParameters, - theProductLine, - thePractitioner); - } - - @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE_2, idempotent = true, type = Measure.class) - public MeasureReport evaluateMeasure2(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) + public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) throws InternalErrorException, FHIRException { // LUKETODO: Parameters within Parameters return myR4MeasureServiceFactory From b74f34c349ea302efbb921a49f06e25dbbc14a36 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 13:45:09 -0500 Subject: [PATCH 15/75] Cleanup. New class for OperationMethodBinding. --- .../java/ca/uhn/fhir/util/ParametersUtil.java | 5 ++ .../ca/uhn/fhir/util/ReflectionUtilTest.java | 2 + .../fhir/rest/server/method/MethodUtil.java | 1 - .../method/OperationIdParamDetails.java | 49 ++++++++++++++ .../uhn/fhir/cr/r4/measure/FooBarParams.java | 67 ------------------- .../r4/measure/MeasureOperationsProvider.java | 29 -------- 6 files changed, 56 insertions(+), 97 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java delete mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index 8339d39b88fd..d350a57f22f7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -636,4 +636,9 @@ public static List extractExamples(Annotation[] theParameterAnnotations) } return retVal; } + + public static boolean isOneOfEligibleTypes(Class theTypeToCheck, Class... theEligibleTypes) { + return Arrays.stream(theEligibleTypes) + .anyMatch(eligibleType -> eligibleType == theTypeToCheck); + } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java index e79917a90a71..94747322e91e 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java @@ -17,6 +17,8 @@ public class ReflectionUtilTest { + // LUKETODO: add tests for + @Test public void testNewInstance() { assertEquals(ArrayList.class, ReflectionUtil.newInstance(ArrayList.class).getClass()); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 7b7668469fcf..34a35b0efd12 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -135,7 +135,6 @@ public static List getResourceParameters( // LUKETODO: handle multiple RequestDetails with an error for (Class parameterType : parameterTypes) { - final IParameter param; // If either the first or second parameter is a RequestDetails, handle it if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { parameters.add(new RequestDetailsParameter()); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java new file mode 100644 index 000000000000..163f72d119eb --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -0,0 +1,49 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.annotation.Nullable; + +import java.util.Optional; + +// LUKETODO: javadoc +class OperationIdParamDetails { + + @Nullable + private final IdParam myIdParam; + + @Nullable + private final Integer myIdParamIndex; + + public static OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); + + public OperationIdParamDetails( + @Nullable IdParam theIdParam, + @Nullable Integer theIdParamIndex) { + myIdParam = theIdParam; + myIdParamIndex = theIdParamIndex; + } + + public boolean isFound() { + return myIdParamIndex != null; + } + + public boolean setOrReturnPreviousValue(boolean thePreviousValue) { + return Optional.ofNullable(myIdParam) + .map(IdParam::optional) + .orElse(thePreviousValue); + } + + public Object[] alterMethodParamsIfNeeded(RequestDetails theRequest, Object[] theMethodParams) { + if (myIdParamIndex == null) { + // no-op + return theMethodParams; + } + + final Object[] clonedMethodParams = theMethodParams.clone(); + + clonedMethodParams[myIdParamIndex] = theRequest.getId(); + + return clonedMethodParams; + } +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java deleted file mode 100644 index 91377d64699e..000000000000 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/FooBarParams.java +++ /dev/null @@ -1,67 +0,0 @@ -package ca.uhn.fhir.cr.r4.measure; - -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.IntegerType; - -import java.util.StringJoiner; - -// LUKETODO: new annotation -// LUKETODO: look at embedded annotations in JPA and follow that pattern -/** - * { - * "resourceType": "OperationDefinition", - * "name": "fooBar", - * "url": "http://foo.bar", - * "parameters": [ - * { - * "name": "doFoo", - * "type: "boolean", - * "use": "in" - * }, - * { - * "name": "count", - * "type: "integer", - * "use": "in" - * }, - * { - * "name": "return", - * "use": "out", - * "type": "OperationOutcome" - * } - * ] - * } - **/ -@OperationEmbeddedType -public class FooBarParams { - // LUKETODO: use OperationEmbeddedParameter instead - @OperationEmbeddedParam(name = "doFoo") - // LUKETODO: do I always need to make it a BooleanType and not a Boolean? - private BooleanType myDoFoo; - - @OperationEmbeddedParam(name = "count") - private IntegerType myCount; - - public FooBarParams(BooleanType myDoFoo, IntegerType myCount) { - this.myDoFoo = myDoFoo; - this.myCount = myCount; - } - - public BooleanType getDoFoo() { - return myDoFoo; - } - - // LUKETODO: do I always need to make it a IntegerType and not an Integer? - public IntegerType getCount() { - return myCount; - } - - @Override - public String toString() { - return new StringJoiner(", ", FooBarParams.class.getSimpleName() + "[", "]") - .add("myDoFoo=" + myDoFoo.getValue()) - .add("myCount=" + myCount.asStringValue()) - .toString(); - } -} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index f4d28ca31a66..d01ff1b68104 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -102,33 +102,4 @@ public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, Requ theParams.getProductLine(), theParams.getPractitioner()); } - - // @Operation(name = "$fooBar", manualResponse = true, idempotent = true) - // OperationOutcome fooBar(FooBarParams theParams) { - // ourLog.info("fooBar params: {}", theParams); - // return new OperationOutcome(); - // } - - @Operation(name = "$fooBar", manualResponse = true, idempotent = true) - // LUKETODO: consider defining a new @OperationEmbeddedParam - public void fooBar(@OperationParam(name = "params") FooBarParams theParams) { - ourLog.info("1234: fooBar params: {}", theParams); - } - // - // @Operation(name = "$returnsBundle", manualResponse = true, idempotent = true) - // public Bundle returnsBundle(@OperationParam(name = "params") ReturnsBundleParams theParams) { - // final Bundle bundle = new Bundle(); - // bundle.setIdentifier(new Identifier().setValue("aValue")); - // - // final String bundleString = - // FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle); - // - // ourLog.info("1234: returnsBundle params: {}, bundle:{}", theParams, bundleString); - // - // return bundle; - // } - - void example() { - fooBar(new FooBarParams(null, null)); - } } From de882c28728e1c284558ec25476fd45824f73435 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 14:08:01 -0500 Subject: [PATCH 16/75] Separate OperationIdParamDetails to use for OperationMethodBinding. Simplify implementation. --- .../method/OperationIdParamDetails.java | 3 +- .../server/method/OperationMethodBinding.java | 48 +++---------------- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index 163f72d119eb..30a53ef7d266 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -13,7 +13,8 @@ class OperationIdParamDetails { private final IdParam myIdParam; @Nullable - private final Integer myIdParamIndex; + // LUKETODO: private + public final Integer myIdParamIndex; public static OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 30fb34d4f1b8..c3088cfcfc3e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -57,7 +57,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -183,12 +182,12 @@ protected OperationMethodBinding( if (getResourceName() == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; - if (myOperationIdParamDetails.myIdParamIndex != null) { + if (myOperationIdParamDetails.isFound()) { myCanOperateAtInstanceLevel = true; } else { myCanOperateAtServerLevel = true; } - } else if (myOperationIdParamDetails.myIdParamIndex == null) { + } else if (! myOperationIdParamDetails.isFound()) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; myCanOperateAtTypeLevel = true; } else { @@ -483,14 +482,14 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM for (Annotation nextParameterAnnotation : parameterAnnotation) { if (nextParameterAnnotation instanceof IdParam) { return new OperationIdParamDetails( - null, (IdParam) nextParameterAnnotation, paramAnnotationIndex, theMethod); + (IdParam) nextParameterAnnotation, paramAnnotationIndex); } } - return new OperationIdParamDetails(null, null, paramAnnotationIndex, theMethod); + return new OperationIdParamDetails(null, paramAnnotationIndex); } } - return new OperationIdParamDetails(null, null, paramAnnotationIndex, theMethod); + return new OperationIdParamDetails(null, paramAnnotationIndex); } @Nonnull @@ -526,7 +525,7 @@ private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( final Annotation fieldAnnotation = fieldAnnotations[0]; if (fieldAnnotation instanceof IdParam) { - return new OperationIdParamDetails(null, (IdParam) fieldAnnotation, paramIndex, theMethod); + return new OperationIdParamDetails((IdParam) fieldAnnotation, paramIndex); } final boolean isRi = theContext.getVersion().getVersion().isRi(); @@ -537,7 +536,7 @@ private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( } } - return new OperationIdParamDetails(null, null, null, theMethod); + return OperationIdParamDetails.EMPTY; } public static class ReturnType { @@ -581,37 +580,4 @@ public void setType(String theType) { myType = theType; } } - - // LUKETODO: consider making this top-level - private static class OperationIdParamDetails { - @Nullable - private final IIdType myIdType; - - @Nullable - private final IdParam myIdParam; - - // LUKETODO: can this ever be null? - @Nullable - private final Integer myIdParamIndex; - - private final Method myMethod; - - // LUKETODO: add a NOTHING factory method - public OperationIdParamDetails( - @Nullable IIdType theIdType, - @Nullable IdParam theIdParam, - @Nullable Integer theIdParamIndex, - Method theMethod) { - myIdType = theIdType; - myIdParam = theIdParam; - myIdParamIndex = theIdParamIndex; - myMethod = theMethod; - } - - public void assigneMethodParamsIfApplicable() {} - - public boolean setOrReturnPreviousValue(boolean thePreviousValue) { - return Optional.ofNullable(myIdParam).map(IdParam::optional).orElse(thePreviousValue); - } - } } From f1fb84fe278d269582605063c4c3cf6ac7ae3776 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 16:54:42 -0500 Subject: [PATCH 17/75] Refactor BaseMethodBinding to be simpler and introduce a new class: BaseMethodBindingMethodParameterBuilder. Also, simplify OperationMethodBinding. --- .../ca/uhn/fhir/rest/param/ParameterUtil.java | 18 +- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 32 +++ .../rest/server/method/BaseMethodBinding.java | 136 +------------ ...seMethodBindingMethodParameterBuilder.java | 183 ++++++++++++++++++ .../server/method/OperationMethodBinding.java | 54 +++--- 5 files changed, 250 insertions(+), 173 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 09c4a087261c..339e8afab1a1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -123,17 +123,21 @@ 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 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; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 1c4847e10705..893affd5ff5f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -21,10 +21,12 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 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; @@ -36,10 +38,12 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; // LUKETODO: consider enhancing this class with operation params stuff instead public class ReflectionUtil { @@ -291,4 +295,32 @@ public static boolean typeExists(String theName) { return false; } } + + // LUKETODO: see if you can get rid of this: + public static boolean hasAnyMethodParamsWithClassesOfAnnotation(Method theMethod, Class theAnnotationClass) { + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + return Arrays.stream(theMethod.getParameterTypes()) + .anyMatch(paramType -> paramType.isAnnotationPresent(theAnnotationClass)); + } + + // LUKETODO: use this whenever possible + public static boolean hasAnyMethodParametersContainingFieldsWithAnnotation(Method theMethod, Class theAnnotationClass) { + return Arrays.stream(theMethod.getParameterTypes()) + .map(Class::getFields) + .map(Arrays::asList) + .flatMap(Collection::stream) + .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); + } + + // + public static List> getMethodParamsWithClassesWithFieldsWithAnnotation(Method theMethod, Class theAnnotationClass) { + return Arrays.stream(theMethod.getParameterTypes()) + .filter(paramType -> hasFieldsWithAnnotation(paramType, theAnnotationClass)) + .collect(Collectors.toUnmodifiableList()); + } + + private static boolean hasFieldsWithAnnotation(Class paramType, Class theAnnotationClass) { + return Arrays.stream(paramType.getDeclaredFields()) + .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 4e752a8a010f..536a9d7df2bb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -34,7 +34,6 @@ import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.Patch; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Search; @@ -46,7 +45,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.BundleProviders; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -58,23 +56,16 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; -import java.util.stream.IntStream; -import static java.util.function.Predicate.not; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseMethodBinding { @@ -119,7 +110,6 @@ protected List getQueryParameters() { return myQueryParameters; } - // LUKETODO: Alternatively, do the mapping here? protected Object[] createMethodParams(RequestDetails theRequest) { ourLog.info("1234: Creating parameters for method {}, and requestDetails: {}", myMethod.getName(), theRequest); Object[] params = new Object[getParameters().size()]; @@ -276,126 +266,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // class, bind to the fields, then invoke. final Method method = getMethod(); - // LUKETODO: split this up into private methods - final Class[] parameterTypes = method.getParameterTypes(); - - ourLog.info( - "1234: invoking method for: {} and params: {} and parameterTypes: {}", - method.getName(), - theMethodParams, - Arrays.toString(parameterTypes)); - - if (Arrays.stream(parameterTypes) - .map(Class::getAnnotations) - .map(Arrays::asList) - .flatMap(Collection::stream) - .anyMatch(OperationEmbeddedType.class::isInstance)) { - - for (Class parameterType : parameterTypes) { - ourLog.info("1234: invoking parameterType: {} and method: {}", parameterType, method.getName()); - final Annotation[] parameterTypeAnnotations = parameterType.getAnnotations(); - - final boolean hasOperationEmbeddedTypeAnnotation = - Arrays.stream(parameterTypeAnnotations).anyMatch(OperationEmbeddedType.class::isInstance); - - if (hasOperationEmbeddedTypeAnnotation) { - final Constructor[] constructors = parameterType.getConstructors(); - - // LUKETODO: what if there's a noarg constructor and all we have is setters? - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - if (constructors.length > 0) { - // LUKETODO: if there are multiple constructors, cycle through them until you find one that - // matches the params list - final Constructor constructor = constructors[0]; - - final Parameter[] constructorParameters = constructor.getParameters(); - - // LUKETODO: mandate an immutable class with a constructor to set params - if (constructorParameters.length == 0) { - throw new InternalErrorException( - Msg.code(234198927) + "No constructor that takes parameters!!!"); - } else { - final Object[] methodParamsWithoutRequestDetails = - removeRequestDetails(theMethodParams); - // LUKETODO: else? - if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { - // LUKETODO: we blow up here because RequestDetails is the EXTRA PARAMETER! - throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); - } - - final int[] requestDetailsIndexes = IntStream.range(0, theMethodParams.length) - .filter(index -> theMethodParams[index] instanceof RequestDetails) - .toArray(); - - if (requestDetailsIndexes.length > 1) { - throw new InternalErrorException(Msg.code(562462) - + "1234: cannot define a request with more than one RequestDetails"); - } - - final Optional optArgPositionRequestDetails = requestDetailsIndexes.length > 0 - ? Optional.of(requestDetailsIndexes[0]) - : Optional.empty(); - - for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { - final Object methodParamAtIndex = methodParamsWithoutRequestDetails[index]; - if (methodParamAtIndex == null) { - // argument is null, so we can't the type, so skip it: - continue; - } - final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); - final Class parameterClassAtIndex = constructorParameters[index].getType(); - - ourLog.info( - "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", - methodParamClassAtIndex, - parameterClassAtIndex); - - // LUKETODO: fix this this is gross - if (Collection.class.isAssignableFrom(methodParamClassAtIndex) - || Collection.class.isAssignableFrom(parameterClassAtIndex)) { - // ex: List and ArrayList - if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { - throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146124), - methodParamClassAtIndex, - parameterClassAtIndex)); - } - } else if (methodParamClassAtIndex != parameterClassAtIndex) { - throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); - } - } - - final Object operationEmbeddedType = - constructor.newInstance(methodParamsWithoutRequestDetails); - - ourLog.info( - "1234: invoking method with operationEmbeddedType: {}", operationEmbeddedType); - // LUKETODO: design for future use factory methods - // LUKETODO: how do I know where to put the RequestDetails???? - if (optArgPositionRequestDetails.isPresent()) { - final Integer requestDetailsIndex = optArgPositionRequestDetails.get(); - - // LUKETODO: review this: this is tacky: - if (requestDetailsIndex == 0) { - return method.invoke(getProvider(), theMethodParams[0], operationEmbeddedType); - } - - return method.invoke( - getProvider(), operationEmbeddedType, theMethodParams[requestDetailsIndex]); - } - return method.invoke(getProvider(), operationEmbeddedType); - } - } - } - } - } - - // LUKETODO: here we fail with: java.lang.IllegalArgumentException: wrong number of arguments - return method.invoke(getProvider(), theMethodParams); + return method.invoke(getProvider(), BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, theMethodParams)); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); @@ -409,11 +280,6 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th } } - private static Object[] removeRequestDetails(Object[] theMethodParams) { - return Arrays.stream(theMethodParams) - .filter(not(RequestDetails.class::isInstance).and(not(SystemRequestDetails.class::isInstance))) - .toArray(); - } /** * Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java new file mode 100644 index 000000000000..34a62abd59bf --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -0,0 +1,183 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.util.function.Predicate.not; + +// LUKETODO: javadoc +class BaseMethodBindingMethodParameterBuilder { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + + static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + final List> parameterTypesWithOperationEmbeddedParam = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation(theMethod, OperationEmbeddedParam.class); + + if (parameterTypesWithOperationEmbeddedParam.size() > 1) { + throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. More than a single such class is part of method definition: %s", Msg.code(924469634), theMethod.getName())); + } + + if (parameterTypesWithOperationEmbeddedParam.isEmpty()) { + return theMethodParams; + } + + if (theMethodParams.length > 2 && + Arrays.stream(theMethodParams).noneMatch(RequestDetails.class::isInstance)) { + throw new InternalErrorException(String.format("%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and one must be a RequestDetails: %s", Msg.code(924469634), theMethod.getName())); + } + + final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); + + return determineMethodParamsForOperationEmbeddedParams(theMethod, parameterTypeWithOperationEmbeddedParam , theMethodParams); + } + + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + private static Object[] determineMethodParamsForOperationEmbeddedParams( + Method theMethod, + Class theParameterTypeWithOperationEmbeddedParam, + Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + + ourLog.info("1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", theParameterTypeWithOperationEmbeddedParam, theMethod.getName()); + + final Object operationEmbeddedType = buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParam, theMethodParams); + + ourLog.info( "1234: build method params with embedded object and requestDetails (if applicable) for: {}", operationEmbeddedType); + + return buildMethodParamsInCorrectPositions(theMethodParams, operationEmbeddedType); + } + + @Nonnull + private static Object buildOperationEmbeddedObject(Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InstantiationException, IllegalAccessException, InvocationTargetException { + final Constructor constructor = validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); + + final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); + + validMethodParamTypes(methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); + + return constructor.newInstance(methodParamsWithoutRequestDetails); + } + + @Nonnull + private static Parameter[] validateAndGetConstructorParameters(Constructor constructor) { + final Parameter[] constructorParameters = constructor.getParameters(); + + // LUKETODO: mandate an immutable class with a constructor to set params + if (constructorParameters.length == 0) { + throw new InternalErrorException( + Msg.code(234198927) + "No constructor that takes parameters!!!"); + } + return constructorParameters; + } + + private static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { + final Constructor[] constructors = theParameterTypeWithOperationEmbeddedParam.getConstructors(); + + if (constructors.length == 0) { + throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. Class has no constructor: %s", Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); + } + + if (constructors.length > 1) { + throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. Class has more than one constructor: %s", Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam)); + } + + return constructors[0]; + } + + // LUKETODO: design for future use factory methods + + @Nonnull + private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodParams, Object operationEmbeddedType) { + + final List requestDetailsIndexes = IntStream.range(0, theMethodParams.length) + .filter(index -> theMethodParams[index] instanceof RequestDetails) + .boxed() + .collect(Collectors.toUnmodifiableList());; + + if (requestDetailsIndexes.size() > 1) { + throw new InternalErrorException(Msg.code(562462) + + "1234: cannot define a request with more than one RequestDetails"); + } + + if (!requestDetailsIndexes.isEmpty()) { + final int requestDetailsIndex = requestDetailsIndexes.get(0); + + if (requestDetailsIndex == 0) { + // RequestDetails goes first + return new Object[] {theMethodParams[0], operationEmbeddedType}; + } + + // RequestDetails goes last + return new Object[] {operationEmbeddedType, theMethodParams[requestDetailsIndex]}; + } + + // No RequestDetails at all + return new Object[] {operationEmbeddedType}; + } + + private static void validMethodParamTypes(Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { + if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { + // LUKETODO: exception message + throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); + } + + for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { + validateMethodParamType( + methodParamsWithoutRequestDetails[index], + constructorParameters[index].getType()); + } + } + + private static void validateMethodParamType(Object methodParamAtIndex, Class parameterClassAtIndex) { + if (methodParamAtIndex == null) { + // argument is null, so we can't the type, so skip it: + return; + } + + final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); + + ourLog.info( + "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", + methodParamClassAtIndex, + parameterClassAtIndex); + + // LUKETODO: fix this this is gross + if (Collection.class.isAssignableFrom(methodParamClassAtIndex) + || Collection.class.isAssignableFrom(parameterClassAtIndex)) { + // ex: List and ArrayList + if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { + throw new InternalErrorException(String.format( + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146124), + methodParamClassAtIndex, + parameterClassAtIndex)); + } + } else if (methodParamClassAtIndex != parameterClassAtIndex) { + throw new InternalErrorException(String.format( + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); + } + } + + // LUKETODO: code reuse? + private static Object[] cloneWithRemovedRequestDetails(Object[] theMethodParams) { + return Arrays.stream(theMethodParams) + .filter(not(RequestDetails.class::isInstance).and(not(SystemRequestDetails.class::isInstance))) + .toArray(); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index c3088cfcfc3e..837e92303f64 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.RequiredParam; @@ -35,13 +35,13 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; +import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBase; @@ -54,7 +54,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -398,8 +397,12 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques Msg.code(428) + message, allowedRequestTypes.toArray(RequestTypeEnum[]::new)); } + ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); + final Object response = - invokeEitherParamsOrEmbeddedParams(theRequest, theMethodParams, myOperationIdParamDetails); + invokeServerMethod( + theRequest, + myOperationIdParamDetails.alterMethodParamsIfNeeded(theRequest, theMethodParams)); if (myManualResponseMode) { return null; @@ -408,18 +411,6 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques return toResourceList(response); } - private Object invokeEitherParamsOrEmbeddedParams( - RequestDetails theRequest, Object[] theMethodParams, OperationIdParamDetails theOperationIdParamDetails) { - ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); - - // LUKETODO: this is gross: fix this: - if (this.myOperationIdParamDetails.myIdParamIndex != null) { - theMethodParams[this.myOperationIdParamDetails.myIdParamIndex] = theRequest.getId(); - } - - return invokeServerMethod(theRequest, theMethodParams); - } - public boolean isCanOperateAtInstanceLevel() { return this.myCanOperateAtInstanceLevel; } @@ -457,11 +448,10 @@ public String getCanonicalUrl() { } private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { - final Class[] parameterTypes = theMethod.getParameterTypes(); - - final List> operationEmbeddedTypes = Arrays.stream(parameterTypes) - .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) - .collect(Collectors.toUnmodifiableList()); + final List> operationEmbeddedTypes = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, + OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty()) { return findIdParamIndexForOperationEmbeddedType(theMethod, operationEmbeddedTypes, theContext); @@ -485,25 +475,30 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM (IdParam) nextParameterAnnotation, paramAnnotationIndex); } } - return new OperationIdParamDetails(null, paramAnnotationIndex); } + + return new OperationIdParamDetails(null, paramAnnotationIndex); } - return new OperationIdParamDetails(null, paramAnnotationIndex); + return OperationIdParamDetails.EMPTY; } @Nonnull private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( Method theMethod, List> theOperationEmbeddedTypes, FhirContext theContext) { - for (Class operationEmbeddedTypes : theOperationEmbeddedTypes) { - if (operationEmbeddedTypes.equals(RequestDetails.class) - || operationEmbeddedTypes.equals(ServletRequestDetails.class)) { + for (Class operationEmbeddedType : theOperationEmbeddedTypes) { + if (ParametersUtil.isOneOfEligibleTypes( + operationEmbeddedType, + RequestDetails.class, + SystemRequestDetails.class)) { // skip } else { - final Field[] fields = operationEmbeddedTypes.getDeclaredFields(); + final Field[] fields = operationEmbeddedType.getDeclaredFields(); int paramIndex = 0; for (Field field : fields) { + ParameterUtil.validateIdType(theMethod, theContext, field.getType()); + final String fieldName = field.getName(); final Annotation[] fieldAnnotations = field.getAnnotations(); @@ -528,9 +523,6 @@ private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( return new OperationIdParamDetails((IdParam) fieldAnnotation, paramIndex); } - final boolean isRi = theContext.getVersion().getVersion().isRi(); - // LUKETODO: usesHapiId - paramIndex++; } } From 6375e56833158af879dde00b556ee76a8c85acbe Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Jan 2025 17:08:29 -0500 Subject: [PATCH 18/75] Get rid of OperationEmbeddedType. --- .../annotation/OperationEmbeddedParam.java | 1 + .../annotation/OperationEmbeddedType.java | 23 ------------ .../ca/uhn/fhir/rest/param/ParameterUtil.java | 35 ------------------- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 1 - .../fhir/rest/server/method/MethodUtil.java | 19 +++++----- .../method/OperationEmbeddedParameter.java | 3 -- .../method/OperationEmbeddedTypeUtils.java | 5 --- .../server/method/OperationEmbeddedUtils.java | 4 --- .../server/method/OperationMethodBinding.java | 12 +++---- .../fhir/cr/r4/measure/CareGapsParams.java | 2 -- .../measure/EvaluateMeasureSingleParams.java | 2 -- 11 files changed, 17 insertions(+), 90 deletions(-) delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java index 3cffe0da0f3d..668bb602cb70 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -33,6 +33,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.PARAMETER, ElementType.FIELD}) // LUKETODO: javadoc to make this clear that it's associated with an OperationEmbeddedParameter +// LUKETODO: get rid of a bunch of cruft like MAX_UNLIMITED public @interface OperationEmbeddedParam { /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java deleted file mode 100644 index 951f69bb3362..000000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedType.java +++ /dev/null @@ -1,23 +0,0 @@ -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: consider not using this but instead using ONLY the replacemenet for @OperationParam -// LUKETODO: better name? -@Retention(RetentionPolicy.RUNTIME) -@Target(value = {ElementType.TYPE}) -public @interface OperationEmbeddedType { - /** - * The name of the embedded type - */ - // LUKETODO: default ""??? - String name() default ""; - - // LUKETODO: javadoc: name of the type - // LUKETODO: extends and defaults what???? - // Class type() default IBase.class; - Class type() default Object.class; -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 339e8afab1a1..1c04269272b6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.util.ReflectionUtil; @@ -39,7 +38,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -153,39 +151,6 @@ public static Integer findParamAnnotationIndex(Method theMethod, Class toFind return null; } - @Nullable - public static Integer findParamAnnotationIndexFromEmbedded(Method theMethod, Class toFind) { - final Class[] parameterTypes = theMethod.getParameterTypes(); - - // LUKETODO: be mindful if we get rid of OperationEmbeddedType - final long operationEmbeddedTypesCount = Arrays.stream(parameterTypes) - .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) - .count(); - - if (operationEmbeddedTypesCount > 1) { - throw new ConfigurationException(String.format( - "%sMore than one parameter with OperationEmbeddedType for method: %s", - Msg.code(99999), theMethod.getName())); - } - - if (operationEmbeddedTypesCount == 0) { - return null; - } - - int paramIndex = 0; - for (Annotation[] annotations : theMethod.getParameterAnnotations()) { - for (Annotation nextAnnotation : annotations) { - Class class1 = nextAnnotation.annotationType(); - if (toFind.isAssignableFrom(class1)) { - return paramIndex; - } - } - paramIndex++; - } - - return null; - } - @Nullable public static Object fromInteger(Class theType, IntegerDt theArgument) { if (theArgument == null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 893affd5ff5f..597c23deb7ac 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -21,7 +21,6 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import jakarta.annotation.Nonnull; import org.apache.commons.lang3.Validate; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 34a35b0efd12..ec4d207d6ca7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -38,7 +38,6 @@ import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Patch; @@ -115,12 +114,13 @@ public static List getResourceParameters( // LUKETODO: one param per method parameter: what happens if we expand this? // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final List> operationEmbeddedTypes = Arrays.stream(parameterTypes) - .filter(paramType -> paramType.isAnnotationPresent(OperationEmbeddedType.class)) - .collect(Collectors.toUnmodifiableList()); + final List> operationEmbeddedTypes = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, + OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty()) { - ourLog.info("1234: isOperationEmbeddedType!!!!!!! method: {}", theMethod.getName()); + ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", theMethod.getName()); // This is the @Operation parameter on the method itself (ex: evaluateMeasure) final Operation op = theMethod.getAnnotation(Operation.class); @@ -128,7 +128,7 @@ public static List getResourceParameters( if (operationEmbeddedTypes.size() > 1) { // LUKETODO: error throw new ConfigurationException(String.format( - "%sOnly one OperationEmbeddedType is supported for now for method: %s", + "%sOnly one type with embedded params is supported for now for method: %s", Msg.code(9999927), theMethod.getName())); } @@ -166,12 +166,12 @@ public static List getResourceParameters( .collect(Collectors.toUnmodifiableSet()); ourLog.info( - "1234: MethodUtil: OperationEmbeddedType: fieldName: {}, class: {}, fieldAnnotations: {}", + "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", fieldName, fieldType.getName(), annotationClassNames); - // This is the parameter on the field in question on the OperationEmbeddedType class: ex + // This is the parameter on the field in question on the type with embedded params class: ex // myCount final Annotation fieldAnnotation = fieldAnnotations[0]; @@ -479,6 +479,7 @@ public static List getResourceParameters( param = new TransactionParameter(theContext); } else if (nextAnnotation instanceof ConditionalUrlParam) { param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); + // LUKETODO: OperationEmbeddedParam: at least for logging for now } else if (nextAnnotation instanceof OperationParam) { Operation op = theMethod.getAnnotation(Operation.class); if (op == null) { @@ -594,7 +595,7 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } - // LUKETODO: if we call this with an @OperationEmbeddedType, we get an Exceptioon here + // LUKETODO: if we call this with an type with embedded params, we get an Exceptioon here // LUKETODO: Or do we expand the paramters here, and then foreaach parameters.add() ??? // ourLog.info("1234: param class: {}, method: {}", param.getClass().getCanonicalName(), // theMethod.getName()); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 3045a99d6438..0e122e0bc4fb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -187,8 +186,6 @@ public void initializeTypes( || isSearchParam || ValidationModeEnum.class.equals(myParameterType); - final boolean isAnnotationPresent = myParameterType.isAnnotationPresent(OperationEmbeddedType.class); - /* * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We * should probably clean this up.. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java deleted file mode 100644 index 787a5166e0ca..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedTypeUtils.java +++ /dev/null @@ -1,5 +0,0 @@ -package ca.uhn.fhir.rest.server.method; - -// LUKETODO: common methods used for this stuff -// LUKETODO: Spring, if applicable -public class OperationEmbeddedTypeUtils {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java deleted file mode 100644 index 32e5534bbd7e..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedUtils.java +++ /dev/null @@ -1,4 +0,0 @@ -package ca.uhn.fhir.rest.server.method; - -// LUKETODO: find good reusable patterns and maintain them here -public class OperationEmbeddedUtils {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 837e92303f64..aa409fe21624 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -454,7 +454,7 @@ private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirCon OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty()) { - return findIdParamIndexForOperationEmbeddedType(theMethod, operationEmbeddedTypes, theContext); + return findIdParamIndexForTypeWithEmbeddedParams(theMethod, operationEmbeddedTypes, theContext); } return getIdParamAnnotationFromMethodParams(theMethod, theContext); @@ -484,16 +484,16 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM } @Nonnull - private OperationIdParamDetails findIdParamIndexForOperationEmbeddedType( - Method theMethod, List> theOperationEmbeddedTypes, FhirContext theContext) { - for (Class operationEmbeddedType : theOperationEmbeddedTypes) { + private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( + Method theMethod, List> theTypesWithEmbeddedParams, FhirContext theContext) { + for (Class typeWithEmbeddedParams : theTypesWithEmbeddedParams) { if (ParametersUtil.isOneOfEligibleTypes( - operationEmbeddedType, + typeWithEmbeddedParams, RequestDetails.class, SystemRequestDetails.class)) { // skip } else { - final Field[] fields = operationEmbeddedType.getDeclaredFields(); + final Field[] fields = typeWithEmbeddedParams.getDeclaredFields(); int paramIndex = 0; for (Field field : fields) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index e0ca6f20d18f..cacb76a1a248 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -1,14 +1,12 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; import java.util.List; import java.util.StringJoiner; -@OperationEmbeddedType public class CareGapsParams { @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 4eccb7eecfdf..491baadddd2d 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; @@ -10,7 +9,6 @@ import java.util.StringJoiner; -@OperationEmbeddedType public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; From 3b2a5656a30a0980c1e81ce31d91b359fa6e5469 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 09:16:58 -0500 Subject: [PATCH 19/75] Begin massive refactoring of MethodUtil#getResourceParameters. Spotless. --- .../java/ca/uhn/fhir/util/ParametersUtil.java | 3 +- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 25 +- .../rest/server/method/BaseMethodBinding.java | 4 +- ...seMethodBindingMethodParameterBuilder.java | 99 ++++--- .../fhir/rest/server/method/MethodUtil.java | 274 ++++++++++++++---- .../method/OperationIdParamDetails.java | 8 +- .../server/method/OperationMethodBinding.java | 23 +- .../r4/measure/CareGapsOperationProvider.java | 2 - .../r4/measure/MeasureOperationsProvider.java | 6 - 9 files changed, 307 insertions(+), 137 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index d350a57f22f7..b5f3d104c19e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -638,7 +638,6 @@ public static List extractExamples(Annotation[] theParameterAnnotations) } public static boolean isOneOfEligibleTypes(Class theTypeToCheck, Class... theEligibleTypes) { - return Arrays.stream(theEligibleTypes) - .anyMatch(eligibleType -> eligibleType == theTypeToCheck); + return Arrays.stream(theEligibleTypes).anyMatch(eligibleType -> eligibleType == theTypeToCheck); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 597c23deb7ac..9cd400600368 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -296,30 +296,33 @@ public static boolean typeExists(String theName) { } // LUKETODO: see if you can get rid of this: - public static boolean hasAnyMethodParamsWithClassesOfAnnotation(Method theMethod, Class theAnnotationClass) { + public static boolean hasAnyMethodParamsWithClassesOfAnnotation( + Method theMethod, Class theAnnotationClass) { // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! return Arrays.stream(theMethod.getParameterTypes()) - .anyMatch(paramType -> paramType.isAnnotationPresent(theAnnotationClass)); + .anyMatch(paramType -> paramType.isAnnotationPresent(theAnnotationClass)); } // LUKETODO: use this whenever possible - public static boolean hasAnyMethodParametersContainingFieldsWithAnnotation(Method theMethod, Class theAnnotationClass) { + public static boolean hasAnyMethodParametersContainingFieldsWithAnnotation( + Method theMethod, Class theAnnotationClass) { return Arrays.stream(theMethod.getParameterTypes()) - .map(Class::getFields) - .map(Arrays::asList) - .flatMap(Collection::stream) - .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); + .map(Class::getFields) + .map(Arrays::asList) + .flatMap(Collection::stream) + .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); } // - public static List> getMethodParamsWithClassesWithFieldsWithAnnotation(Method theMethod, Class theAnnotationClass) { + public static List> getMethodParamsWithClassesWithFieldsWithAnnotation( + Method theMethod, Class theAnnotationClass) { return Arrays.stream(theMethod.getParameterTypes()) - .filter(paramType -> hasFieldsWithAnnotation(paramType, theAnnotationClass)) - .collect(Collectors.toUnmodifiableList()); + .filter(paramType -> hasFieldsWithAnnotation(paramType, theAnnotationClass)) + .collect(Collectors.toUnmodifiableList()); } private static boolean hasFieldsWithAnnotation(Class paramType, Class theAnnotationClass) { return Arrays.stream(paramType.getDeclaredFields()) - .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); + .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 536a9d7df2bb..67c3f67265cf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -266,7 +266,8 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // class, bind to the fields, then invoke. final Method method = getMethod(); - return method.invoke(getProvider(), BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, theMethodParams)); + return method.invoke( + getProvider(), BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, theMethodParams)); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); @@ -280,7 +281,6 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th } } - /** * Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally. */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 34a62abd59bf..94554ccbbe8b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -23,47 +23,61 @@ // LUKETODO: javadoc class BaseMethodBindingMethodParameterBuilder { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + private static final org.slf4j.Logger ourLog = + org.slf4j.LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); - static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) + throws InvocationTargetException, IllegalAccessException, InstantiationException { final List> parameterTypesWithOperationEmbeddedParam = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation(theMethod, OperationEmbeddedParam.class); + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, OperationEmbeddedParam.class); if (parameterTypesWithOperationEmbeddedParam.size() > 1) { - throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. More than a single such class is part of method definition: %s", Msg.code(924469634), theMethod.getName())); + throw new InternalErrorException(String.format( + "%s1234: Invalid operation embedded parameters. More than a single such class is part of method definition: %s", + Msg.code(924469634), theMethod.getName())); } if (parameterTypesWithOperationEmbeddedParam.isEmpty()) { return theMethodParams; } - if (theMethodParams.length > 2 && - Arrays.stream(theMethodParams).noneMatch(RequestDetails.class::isInstance)) { - throw new InternalErrorException(String.format("%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and one must be a RequestDetails: %s", Msg.code(924469634), theMethod.getName())); + if (theMethodParams.length > 2 && Arrays.stream(theMethodParams).noneMatch(RequestDetails.class::isInstance)) { + throw new InternalErrorException(String.format( + "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and one must be a RequestDetails: %s", + Msg.code(924469634), theMethod.getName())); } final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); - return determineMethodParamsForOperationEmbeddedParams(theMethod, parameterTypeWithOperationEmbeddedParam , theMethodParams); + return determineMethodParamsForOperationEmbeddedParams( + theMethod, parameterTypeWithOperationEmbeddedParam, theMethodParams); } // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! private static Object[] determineMethodParamsForOperationEmbeddedParams( - Method theMethod, - Class theParameterTypeWithOperationEmbeddedParam, - Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + Method theMethod, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + throws InvocationTargetException, IllegalAccessException, InstantiationException { - ourLog.info("1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", theParameterTypeWithOperationEmbeddedParam, theMethod.getName()); + ourLog.info( + "1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", + theParameterTypeWithOperationEmbeddedParam, + theMethod.getName()); - final Object operationEmbeddedType = buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParam, theMethodParams); + final Object operationEmbeddedType = + buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParam, theMethodParams); - ourLog.info( "1234: build method params with embedded object and requestDetails (if applicable) for: {}", operationEmbeddedType); + ourLog.info( + "1234: build method params with embedded object and requestDetails (if applicable) for: {}", + operationEmbeddedType); return buildMethodParamsInCorrectPositions(theMethodParams, operationEmbeddedType); } @Nonnull - private static Object buildOperationEmbeddedObject(Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InstantiationException, IllegalAccessException, InvocationTargetException { + private static Object buildOperationEmbeddedObject( + Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + throws InstantiationException, IllegalAccessException, InvocationTargetException { final Constructor constructor = validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); @@ -79,8 +93,7 @@ private static Parameter[] validateAndGetConstructorParameters(Constructor co // LUKETODO: mandate an immutable class with a constructor to set params if (constructorParameters.length == 0) { - throw new InternalErrorException( - Msg.code(234198927) + "No constructor that takes parameters!!!"); + throw new InternalErrorException(Msg.code(234198927) + "No constructor that takes parameters!!!"); } return constructorParameters; } @@ -89,11 +102,15 @@ private static Constructor validateAndGetConstructor(Class theParameterTyp final Constructor[] constructors = theParameterTypeWithOperationEmbeddedParam.getConstructors(); if (constructors.length == 0) { - throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. Class has no constructor: %s", Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); + throw new InternalErrorException(String.format( + "%s1234: Invalid operation embedded parameters. Class has no constructor: %s", + Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); } if (constructors.length > 1) { - throw new InternalErrorException(String.format("%s1234: Invalid operation embedded parameters. Class has more than one constructor: %s", Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam)); + throw new InternalErrorException(String.format( + "%s1234: Invalid operation embedded parameters. Class has more than one constructor: %s", + Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam)); } return constructors[0]; @@ -102,16 +119,19 @@ private static Constructor validateAndGetConstructor(Class theParameterTyp // LUKETODO: design for future use factory methods @Nonnull - private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodParams, Object operationEmbeddedType) { + private static Object[] buildMethodParamsInCorrectPositions( + Object[] theMethodParams, Object operationEmbeddedType) { + // LUKETODO: this is DUMB: extract the Request Details, then pass an enum of either FIRST OR LAST final List requestDetailsIndexes = IntStream.range(0, theMethodParams.length) - .filter(index -> theMethodParams[index] instanceof RequestDetails) - .boxed() - .collect(Collectors.toUnmodifiableList());; + .filter(index -> theMethodParams[index] instanceof RequestDetails) + .boxed() + .collect(Collectors.toUnmodifiableList()); + ; if (requestDetailsIndexes.size() > 1) { - throw new InternalErrorException(Msg.code(562462) - + "1234: cannot define a request with more than one RequestDetails"); + throw new InternalErrorException( + Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); } if (!requestDetailsIndexes.isEmpty()) { @@ -130,16 +150,15 @@ private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodPa return new Object[] {operationEmbeddedType}; } - private static void validMethodParamTypes(Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { + private static void validMethodParamTypes( + Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { // LUKETODO: exception message throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); } for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { - validateMethodParamType( - methodParamsWithoutRequestDetails[index], - constructorParameters[index].getType()); + validateMethodParamType(methodParamsWithoutRequestDetails[index], constructorParameters[index].getType()); } } @@ -152,32 +171,30 @@ private static void validateMethodParamType(Object methodParamAtIndex, Class final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); ourLog.info( - "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", - methodParamClassAtIndex, - parameterClassAtIndex); + "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", + methodParamClassAtIndex, + parameterClassAtIndex); // LUKETODO: fix this this is gross if (Collection.class.isAssignableFrom(methodParamClassAtIndex) - || Collection.class.isAssignableFrom(parameterClassAtIndex)) { + || Collection.class.isAssignableFrom(parameterClassAtIndex)) { // ex: List and ArrayList if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146124), - methodParamClassAtIndex, - parameterClassAtIndex)); + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex)); } } else if (methodParamClassAtIndex != parameterClassAtIndex) { throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); + "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", + Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); } } // LUKETODO: code reuse? private static Object[] cloneWithRemovedRequestDetails(Object[] theMethodParams) { return Arrays.stream(theMethodParams) - .filter(not(RequestDetails.class::isInstance).and(not(SystemRequestDetails.class::isInstance))) - .toArray(); + .filter(not(RequestDetails.class::isInstance).and(not(SystemRequestDetails.class::isInstance))) + .toArray(); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index ec4d207d6ca7..12f72fb9c7ec 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -71,6 +71,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.time.Year; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -108,18 +109,17 @@ public static List getResourceParameters( ourLog.info("1234: getResourceParameters: " + theMethod.getName()); List parameters = new ArrayList<>(); - // LUKETODO: why no caregaps here???? Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; // LUKETODO: one param per method parameter: what happens if we expand this? // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final List> operationEmbeddedTypes = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, - OperationEmbeddedParam.class); + final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, OperationEmbeddedParam.class); - if (!operationEmbeddedTypes.isEmpty()) { + if (!operationEmbeddedTypes.isEmpty() + /*&& Year.now().equals(Year.of(1900))*/ + ) { // disable for now ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", theMethod.getName()); // This is the @Operation parameter on the method itself (ex: evaluateMeasure) @@ -179,7 +179,6 @@ public static List getResourceParameters( if (fieldAnnotation instanceof IdParam) { parameters.add(new NullParameter()); } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - // LUKETODO: use OperationEmbeddedParam instead final OperationEmbeddedParam operationParam = (OperationEmbeddedParam) fieldAnnotation; final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; @@ -284,8 +283,8 @@ public static List getResourceParameters( } for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { - - IParameter param = null; + // These can be multiples if we're dealing with a OperationEmbeddedParam + final List params = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -293,7 +292,7 @@ public static List getResourceParameters( Class> innerCollectionType = null; if (TagList.class.isAssignableFrom(parameterType)) { // TagList is handled directly within the method bindings - param = new NullParameter(); + params.add(new NullParameter()); } else { if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; @@ -354,24 +353,24 @@ public static List getResourceParameters( } if (ServletRequest.class.isAssignableFrom(parameterType)) { - param = new ServletRequestParameter(); + params.add(new ServletRequestParameter()); } else if (ServletResponse.class.isAssignableFrom(parameterType)) { - param = new ServletResponseParameter(); + params.add(new ServletResponseParameter()); } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { - param = new RequestDetailsParameter(); + params.add(new RequestDetailsParameter()); } else if (parameterType.equals(IInterceptorBroadcaster.class)) { - param = new InterceptorBroadcasterParameter(); + params.add(new InterceptorBroadcasterParameter()); } else if (parameterType.equals(SummaryEnum.class)) { - param = new SummaryEnumParameter(); + params.add(new SummaryEnumParameter()); } else if (parameterType.equals(PatchTypeEnum.class)) { - param = new PatchTypeParameter(); + params.add(new PatchTypeParameter()); } else if (parameterType.equals(SearchContainedModeEnum.class)) { - param = new SearchContainedModeParameter(); + params.add(new SearchContainedModeParameter()); } else if (parameterType.equals(SearchTotalModeEnum.class)) { - param = new SearchTotalModeParameter(); + params.add(new SearchTotalModeParameter()); } else { - for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { + for (int i = 0; i < nextParameterAnnotations.length && params.isEmpty(); i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; if (nextAnnotation instanceof RequiredParam) { @@ -385,7 +384,7 @@ public static List getResourceParameters( ((RequiredParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); MethodUtil.extractDescription(parameter, nextParameterAnnotations); - param = parameter; + params.add(parameter); } else if (nextAnnotation instanceof OptionalParam) { SearchParameter parameter = new SearchParameter(); parameter.setName(((OptionalParam) nextAnnotation).name()); @@ -397,9 +396,9 @@ public static List getResourceParameters( ((OptionalParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); MethodUtil.extractDescription(parameter, nextParameterAnnotations); - param = parameter; + params.add(parameter); } else if (nextAnnotation instanceof RawParam) { - param = new RawParamsParameter(parameters); + params.add(new RawParamsParameter(parameters)); } else if (nextAnnotation instanceof IncludeParam) { Class> instantiableCollectionType; Class specType; @@ -420,8 +419,8 @@ public static List getResourceParameters( specType = parameterType; } - param = new IncludeParameter( - (IncludeParam) nextAnnotation, instantiableCollectionType, specType); + params.add(new IncludeParameter( + (IncludeParam) nextAnnotation, instantiableCollectionType, specType)); } else if (nextAnnotation instanceof ResourceParam) { Mode mode; if (IBaseResource.class.isAssignableFrom(parameterType)) { @@ -445,41 +444,41 @@ public static List getResourceParameters( } boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null; - param = new ResourceParameter( + params.add(new ResourceParameter( (Class) parameterType, theProvider, mode, methodIsOperation, - methodIsPatch); + methodIsPatch)); } else if (nextAnnotation instanceof IdParam) { - param = new NullParameter(); + params.add(new NullParameter()); } else if (nextAnnotation instanceof ServerBase) { - param = new ServerBaseParamBinder(); + params.add(new ServerBaseParamBinder()); } else if (nextAnnotation instanceof Elements) { - param = new ElementsParameter(); + params.add(new ElementsParameter()); } else if (nextAnnotation instanceof Since) { - param = new SinceParameter(); - ((SinceParameter) param) - .setType(theContext, parameterType, innerCollectionType, outerCollectionType); + final SinceParameter sinceParameter = new SinceParameter(); + sinceParameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); + params.add(sinceParameter); } else if (nextAnnotation instanceof At) { - param = new AtParameter(); - ((AtParameter) param) - .setType(theContext, parameterType, innerCollectionType, outerCollectionType); + final AtParameter atParameter = new AtParameter(); + atParameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); + params.add(atParameter); } else if (nextAnnotation instanceof Count) { - param = new CountParameter(); + params.add(new CountParameter()); } else if (nextAnnotation instanceof Offset) { - param = new OffsetParameter(); + params.add(new OffsetParameter()); } else if (nextAnnotation instanceof GraphQLQueryUrl) { - param = new GraphQLQueryUrlParameter(); + params.add(new GraphQLQueryUrlParameter()); } else if (nextAnnotation instanceof GraphQLQueryBody) { - param = new GraphQLQueryBodyParameter(); + params.add(new GraphQLQueryBodyParameter()); } else if (nextAnnotation instanceof Sort) { - param = new SortParameter(theContext); + params.add(new SortParameter(theContext)); } else if (nextAnnotation instanceof TransactionParam) { - param = new TransactionParameter(theContext); + params.add(new TransactionParameter(theContext)); } else if (nextAnnotation instanceof ConditionalUrlParam) { - param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); - // LUKETODO: OperationEmbeddedParam: at least for logging for now + params.add( + new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple())); } else if (nextAnnotation instanceof OperationParam) { Operation op = theMethod.getAnnotation(Operation.class); if (op == null) { @@ -488,18 +487,182 @@ public static List getResourceParameters( + theMethod.toGenericString()); } + // LUKETODO: try to combine into validateAndGet methods + final List> operationEmbeddedTypesInner = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, OperationEmbeddedParam.class); + + if (operationEmbeddedTypesInner.size() > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sOnly one type with embedded params is supported for now for method: %s", + Msg.code(9999927), theMethod.getName())); + } + + if (!operationEmbeddedTypes.isEmpty() && Year.now().equals(Year.of(1900))) { + // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE + // METHOD!!!!!!!!!!!!!!!!!!!! + ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", theMethod.getName()); + + final Class operationEmbeddedType = operationEmbeddedTypesInner.get(0); + + final Field[] fields = operationEmbeddedType.getDeclaredFields(); + + for (Field field : fields) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final Annotation[] fieldAnnotations = field.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), fieldName, theMethod.getName())); + } + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, theMethod.getName())); + } + + final Set annotationClassNames = Arrays.stream(fieldAnnotations) + .map(Annotation::annotationType) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); + + ourLog.info( + "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", + fieldName, + fieldType.getName(), + annotationClassNames); + + // This is the parameter on the field in question on the type with embedded params + // class: ex + // myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + // skip + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + final OperationEmbeddedParam operationParam = + (OperationEmbeddedParam) fieldAnnotation; + + final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning + // repo + final OperationEmbeddedParameter operationParameter = + new OperationEmbeddedParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + + Class> outerCollectionTypeInner = null; + Class> innerCollectionTypeInner = null; + + parameterType = fieldType; + + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionTypeInner = + (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + if (parameterType == null + && theMethod.getDeclaringClass().isSynthetic()) { + try { + theMethod = theMethod + .getDeclaringClass() + .getSuperclass() + .getMethod(theMethod.getName(), parameterTypes); + parameterType = + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter( + theMethod, paramIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + theMethod.getName() + "' does not exist for super class '" + + theMethod + .getDeclaringClass() + .getSuperclass() + "'"); + } + } + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: now we're processing the generic parameter, so capture the inner and + // outer + // types + // Collection + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionTypeInner = innerCollectionTypeInner; + innerCollectionTypeInner = + (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: as a guard: if this is still a Collection, then throw because + // something went + // wrong + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException( + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + + theMethod.getName() + + "' in type '" + + theMethod + .getDeclaringClass() + .getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + // LUKETODO: do I need to worry about this: + /* + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + theMethod); + } + parameterType = newParameterType; + */ + + ourLog.info( + "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", + theMethod.getName(), + outerCollectionType, + innerCollectionType, + parameterType); + + // LUKETODO: how to handle multiple parameters.add(param); ????? + } else { + // some kind of Exception for now? + } + } + } + OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( + params.add(new OperationParameter( theContext, op.name(), operationParam.name(), operationParam.min(), operationParam.max(), description, - examples); + examples)); if (isNotBlank(operationParam.typeName())) { BaseRuntimeElementDefinition elementDefinition = theContext.getElementDefinition(operationParam.typeName()); @@ -526,7 +689,7 @@ public static List getResourceParameters( } String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( + params.add(new OperationParameter( theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, @@ -553,7 +716,7 @@ public Object outgoingClient(Object theObject) { return ParametersUtil.createString( theContext, ((ValidationModeEnum) theObject).getCode()); } - }); + })); } else if (nextAnnotation instanceof Validate.Profile) { if (!parameterType.equals(String.class)) { throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" @@ -562,7 +725,7 @@ public Object outgoingClient(Object theObject) { } String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( + params.add(new OperationParameter( theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, @@ -580,14 +743,14 @@ public Object incomingServer(Object theObject) { public Object outgoingClient(Object theObject) { return ParametersUtil.createString(theContext, theObject.toString()); } - }); + })); } else { continue; } } } - if (param == null) { + if (params.isEmpty()) { throw new ConfigurationException( Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" @@ -606,9 +769,16 @@ public Object outgoingClient(Object theObject) { outerCollectionType, innerCollectionType, parameterType); - param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); - parameters.add(param); + for (IParameter param : params) { + // LUKETODO: this may not work because of the multitude of values for each of the 3 last params for + // OperationEmbeddedParam + param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); + } + + parameters.addAll(params); + + // LUKETODO: what to do about this? paramIndex++; } return parameters; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index 30a53ef7d266..d3cd52000a7f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -18,9 +18,7 @@ class OperationIdParamDetails { public static OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); - public OperationIdParamDetails( - @Nullable IdParam theIdParam, - @Nullable Integer theIdParamIndex) { + public OperationIdParamDetails(@Nullable IdParam theIdParam, @Nullable Integer theIdParamIndex) { myIdParam = theIdParam; myIdParamIndex = theIdParamIndex; } @@ -30,9 +28,7 @@ public boolean isFound() { } public boolean setOrReturnPreviousValue(boolean thePreviousValue) { - return Optional.ofNullable(myIdParam) - .map(IdParam::optional) - .orElse(thePreviousValue); + return Optional.ofNullable(myIdParam).map(IdParam::optional).orElse(thePreviousValue); } public Object[] alterMethodParamsIfNeeded(RequestDetails theRequest, Object[] theMethodParams) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index aa409fe21624..9998aae0fec5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -186,7 +186,7 @@ protected OperationMethodBinding( } else { myCanOperateAtServerLevel = true; } - } else if (! myOperationIdParamDetails.isFound()) { + } else if (!myOperationIdParamDetails.isFound()) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; myCanOperateAtTypeLevel = true; } else { @@ -399,10 +399,8 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); - final Object response = - invokeServerMethod( - theRequest, - myOperationIdParamDetails.alterMethodParamsIfNeeded(theRequest, theMethodParams)); + final Object response = invokeServerMethod( + theRequest, myOperationIdParamDetails.alterMethodParamsIfNeeded(theRequest, theMethodParams)); if (myManualResponseMode) { return null; @@ -448,10 +446,8 @@ public String getCanonicalUrl() { } private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { - final List> operationEmbeddedTypes = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, - OperationEmbeddedParam.class); + final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty()) { return findIdParamIndexForTypeWithEmbeddedParams(theMethod, operationEmbeddedTypes, theContext); @@ -471,8 +467,7 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM for (Annotation nextParameterAnnotation : parameterAnnotation) { if (nextParameterAnnotation instanceof IdParam) { - return new OperationIdParamDetails( - (IdParam) nextParameterAnnotation, paramAnnotationIndex); + return new OperationIdParamDetails((IdParam) nextParameterAnnotation, paramAnnotationIndex); } } } @@ -485,12 +480,10 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM @Nonnull private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( - Method theMethod, List> theTypesWithEmbeddedParams, FhirContext theContext) { + Method theMethod, List> theTypesWithEmbeddedParams, FhirContext theContext) { for (Class typeWithEmbeddedParams : theTypesWithEmbeddedParams) { if (ParametersUtil.isOneOfEligibleTypes( - typeWithEmbeddedParams, - RequestDetails.class, - SystemRequestDetails.class)) { + typeWithEmbeddedParams, RequestDetails.class, SystemRequestDetails.class)) { // skip } else { final Field[] fields = typeWithEmbeddedParams.getDeclaredFields(); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index e7db933df1f5..7a4b9a53e844 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -27,14 +27,12 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; import java.util.Optional; import java.util.stream.Collectors; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index d01ff1b68104..17ce9e9b38f0 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -21,19 +21,13 @@ import ca.uhn.fhir.cr.common.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; -import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Endpoint; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; -import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.utility.monad.Eithers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 1b9a256f6a9d89519aa93e68923918439a41e41f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 10:24:21 -0500 Subject: [PATCH 20/75] Distinguish between theMethod parameter and Method variable that's mutated during execution. --- .../fhir/rest/server/method/MethodUtil.java | 223 +++++++++--------- 1 file changed, 112 insertions(+), 111 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 12f72fb9c7ec..4901903cc661 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -105,31 +105,33 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] @SuppressWarnings("unchecked") public static List getResourceParameters( - final FhirContext theContext, Method theMethod, Object theProvider) { - ourLog.info("1234: getResourceParameters: " + theMethod.getName()); + final FhirContext theContext, final Method theMethod, Object theProvider) { + // This variable will be mutated so distinguish it from the argument to getResourceParameters() + Method methodToUse = theMethod; + ourLog.info("1234: getResourceParameters: " + methodToUse.getName()); List parameters = new ArrayList<>(); - Class[] parameterTypes = theMethod.getParameterTypes(); + Class[] parameterTypes = methodToUse.getParameterTypes(); int paramIndex = 0; // LUKETODO: one param per method parameter: what happens if we expand this? // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, OperationEmbeddedParam.class); + methodToUse, OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty() /*&& Year.now().equals(Year.of(1900))*/ ) { // disable for now - ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", theMethod.getName()); + ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); // This is the @Operation parameter on the method itself (ex: evaluateMeasure) - final Operation op = theMethod.getAnnotation(Operation.class); + final Operation op = methodToUse.getAnnotation(Operation.class); if (operationEmbeddedTypes.size() > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), theMethod.getName())); + Msg.code(9999927), methodToUse.getName())); } // LUKETODO: handle multiple RequestDetails with an error @@ -150,14 +152,14 @@ public static List getResourceParameters( if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, theMethod.getName())); + Msg.code(9999926), fieldName, methodToUse.getName())); } if (fieldAnnotations.length > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, theMethod.getName())); + Msg.code(999998), fieldName, methodToUse.getName())); } final Set annotationClassNames = Arrays.stream(fieldAnnotations) @@ -206,20 +208,22 @@ public static List getResourceParameters( // LUKETODO: come up with another method to do this for field params parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); if (parameterType == null - && theMethod.getDeclaringClass().isSynthetic()) { + && methodToUse.getDeclaringClass().isSynthetic()) { try { - theMethod = theMethod + methodToUse = methodToUse .getDeclaringClass() .getSuperclass() - .getMethod(theMethod.getName(), parameterTypes); + .getMethod(methodToUse.getName(), parameterTypes); parameterType = // LUKETODO: what to do here if anything? ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - theMethod, paramIndex); + methodToUse, paramIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" - + theMethod.getName() + "' does not exist for super class '" - + theMethod.getDeclaringClass().getSuperclass() + "'"); + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse + .getDeclaringClass() + .getSuperclass() + "'"); } } // LUKETODO: @@ -242,9 +246,12 @@ public static List getResourceParameters( // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName() + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + + methodToUse.getName() + "' in type '" - + theMethod.getDeclaringClass().getCanonicalName() + + methodToUse + .getDeclaringClass() + .getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } @@ -254,20 +261,20 @@ public static List getResourceParameters( Class newParameterType = elementDefinition.getImplementingClass(); if (!declaredParameterType.isAssignableFrom(newParameterType)) { throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + theMethod); + + operationParam.typeName() + "\" specified on method " + methodToUse); } parameterType = newParameterType; */ ourLog.info( "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - theMethod.getName(), + methodToUse.getName(), outerCollectionType, innerCollectionType, parameterType); operationParameter.initializeTypes( - theMethod, outerCollectionType, innerCollectionType, parameterType); + methodToUse, outerCollectionType, innerCollectionType, parameterType); parameters.add(operationParameter); } else { @@ -282,9 +289,8 @@ public static List getResourceParameters( return parameters; } - for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { - // These can be multiples if we're dealing with a OperationEmbeddedParam - final List params = new ArrayList<>(); + for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { + IParameter param = null; Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -292,23 +298,23 @@ public static List getResourceParameters( Class> innerCollectionType = null; if (TagList.class.isAssignableFrom(parameterType)) { // TagList is handled directly within the method bindings - params.add(new NullParameter()); + param = new NullParameter(); } else { if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); - if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); + if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { try { - theMethod = theMethod + methodToUse = methodToUse .getDeclaringClass() .getSuperclass() - .getMethod(theMethod.getName(), parameterTypes); + .getMethod(methodToUse.getName(), parameterTypes); parameterType = - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); + ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" - + theMethod.getName() + "' does not exist for super class '" - + theMethod.getDeclaringClass().getSuperclass() + "'"); + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse.getDeclaringClass().getSuperclass() + "'"); } } declaredParameterType = parameterType; @@ -319,15 +325,15 @@ public static List getResourceParameters( if (Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); declaredParameterType = parameterType; } // LUKETODO: as a guard: if this is still a Collection, then throw because something went wrong if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName() + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() + "' in type '" - + theMethod.getDeclaringClass().getCanonicalName() + + methodToUse.getDeclaringClass().getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } @@ -341,7 +347,7 @@ public static List getResourceParameters( */ if (IPrimitiveType.class.equals(parameterType)) { Class genericType = - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); + ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); if (Date.class.equals(genericType)) { BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); parameterType = dateTimeDef.getImplementingClass(); @@ -353,24 +359,24 @@ public static List getResourceParameters( } if (ServletRequest.class.isAssignableFrom(parameterType)) { - params.add(new ServletRequestParameter()); + param = new ServletRequestParameter(); } else if (ServletResponse.class.isAssignableFrom(parameterType)) { - params.add(new ServletResponseParameter()); + param = new ServletResponseParameter(); } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { - params.add(new RequestDetailsParameter()); + param = new RequestDetailsParameter(); } else if (parameterType.equals(IInterceptorBroadcaster.class)) { - params.add(new InterceptorBroadcasterParameter()); + param = new InterceptorBroadcasterParameter(); } else if (parameterType.equals(SummaryEnum.class)) { - params.add(new SummaryEnumParameter()); + param = new SummaryEnumParameter(); } else if (parameterType.equals(PatchTypeEnum.class)) { - params.add(new PatchTypeParameter()); + param = new PatchTypeParameter(); } else if (parameterType.equals(SearchContainedModeEnum.class)) { - params.add(new SearchContainedModeParameter()); + param = new SearchContainedModeParameter(); } else if (parameterType.equals(SearchTotalModeEnum.class)) { - params.add(new SearchTotalModeParameter()); + param = new SearchTotalModeParameter(); } else { - for (int i = 0; i < nextParameterAnnotations.length && params.isEmpty(); i++) { + for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; if (nextAnnotation instanceof RequiredParam) { @@ -384,7 +390,7 @@ public static List getResourceParameters( ((RequiredParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); MethodUtil.extractDescription(parameter, nextParameterAnnotations); - params.add(parameter); + param = parameter; } else if (nextAnnotation instanceof OptionalParam) { SearchParameter parameter = new SearchParameter(); parameter.setName(((OptionalParam) nextAnnotation).name()); @@ -396,9 +402,9 @@ public static List getResourceParameters( ((OptionalParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); MethodUtil.extractDescription(parameter, nextParameterAnnotations); - params.add(parameter); + param = parameter; } else if (nextAnnotation instanceof RawParam) { - params.add(new RawParamsParameter(parameters)); + param = new RawParamsParameter(parameters); } else if (nextAnnotation instanceof IncludeParam) { Class> instantiableCollectionType; Class specType; @@ -409,18 +415,18 @@ public static List getResourceParameters( } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) { - throw new ConfigurationException(Msg.code(402) + "Method '" + theMethod.getName() + throw new ConfigurationException(Msg.code(402) + "Method '" + methodToUse.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" + Include.class.getSimpleName() + ">"); } else { instantiableCollectionType = (Class>) CollectionBinder.getInstantiableCollectionType( - innerCollectionType, "Method '" + theMethod.getName() + "'"); + innerCollectionType, "Method '" + methodToUse.getName() + "'"); specType = parameterType; } - params.add(new IncludeParameter( - (IncludeParam) nextAnnotation, instantiableCollectionType, specType)); + param = new IncludeParameter( + (IncludeParam) nextAnnotation, instantiableCollectionType, specType); } else if (nextAnnotation instanceof ResourceParam) { Mode mode; if (IBaseResource.class.isAssignableFrom(parameterType)) { @@ -434,7 +440,7 @@ public static List getResourceParameters( } else { StringBuilder b = new StringBuilder(); b.append("Method '"); - b.append(theMethod.getName()); + b.append(methodToUse.getName()); b.append("' is annotated with @"); b.append(ResourceParam.class.getSimpleName()); b.append(" but has a type that is not an implementation of "); @@ -442,67 +448,66 @@ public static List getResourceParameters( b.append(" or String or byte[]"); throw new ConfigurationException(Msg.code(403) + b.toString()); } - boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; - boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null; - params.add(new ResourceParameter( + boolean methodIsOperation = methodToUse.getAnnotation(Operation.class) != null; + boolean methodIsPatch = methodToUse.getAnnotation(Patch.class) != null; + param = new ResourceParameter( (Class) parameterType, theProvider, mode, methodIsOperation, - methodIsPatch)); + methodIsPatch); } else if (nextAnnotation instanceof IdParam) { - params.add(new NullParameter()); + param = new NullParameter(); } else if (nextAnnotation instanceof ServerBase) { - params.add(new ServerBaseParamBinder()); + param = new ServerBaseParamBinder(); } else if (nextAnnotation instanceof Elements) { - params.add(new ElementsParameter()); + param = new ElementsParameter(); } else if (nextAnnotation instanceof Since) { - final SinceParameter sinceParameter = new SinceParameter(); - sinceParameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - params.add(sinceParameter); + param = new SinceParameter(); + ((SinceParameter) param) + .setType(theContext, parameterType, innerCollectionType, outerCollectionType); } else if (nextAnnotation instanceof At) { - final AtParameter atParameter = new AtParameter(); - atParameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - params.add(atParameter); + param = new AtParameter(); + ((AtParameter) param) + .setType(theContext, parameterType, innerCollectionType, outerCollectionType); } else if (nextAnnotation instanceof Count) { - params.add(new CountParameter()); + param = new CountParameter(); } else if (nextAnnotation instanceof Offset) { - params.add(new OffsetParameter()); + param = new OffsetParameter(); } else if (nextAnnotation instanceof GraphQLQueryUrl) { - params.add(new GraphQLQueryUrlParameter()); + param = new GraphQLQueryUrlParameter(); } else if (nextAnnotation instanceof GraphQLQueryBody) { - params.add(new GraphQLQueryBodyParameter()); + param = new GraphQLQueryBodyParameter(); } else if (nextAnnotation instanceof Sort) { - params.add(new SortParameter(theContext)); + param = new SortParameter(theContext); } else if (nextAnnotation instanceof TransactionParam) { - params.add(new TransactionParameter(theContext)); + param = new TransactionParameter(theContext); } else if (nextAnnotation instanceof ConditionalUrlParam) { - params.add( - new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple())); + param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); } else if (nextAnnotation instanceof OperationParam) { - Operation op = theMethod.getAnnotation(Operation.class); + Operation op = methodToUse.getAnnotation(Operation.class); if (op == null) { throw new ConfigurationException(Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: " - + theMethod.toGenericString()); + + methodToUse.toGenericString()); } // LUKETODO: try to combine into validateAndGet methods final List> operationEmbeddedTypesInner = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, OperationEmbeddedParam.class); + methodToUse, OperationEmbeddedParam.class); if (operationEmbeddedTypesInner.size() > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), theMethod.getName())); + Msg.code(9999927), methodToUse.getName())); } if (!operationEmbeddedTypes.isEmpty() && Year.now().equals(Year.of(1900))) { // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE // METHOD!!!!!!!!!!!!!!!!!!!! - ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", theMethod.getName()); + ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); final Class operationEmbeddedType = operationEmbeddedTypesInner.get(0); @@ -516,14 +521,14 @@ public static List getResourceParameters( if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, theMethod.getName())); + Msg.code(9999926), fieldName, methodToUse.getName())); } if (fieldAnnotations.length > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, theMethod.getName())); + Msg.code(999998), fieldName, methodToUse.getName())); } final Set annotationClassNames = Arrays.stream(fieldAnnotations) @@ -576,20 +581,22 @@ public static List getResourceParameters( // LUKETODO: come up with another method to do this for field params parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); if (parameterType == null - && theMethod.getDeclaringClass().isSynthetic()) { + && methodToUse + .getDeclaringClass() + .isSynthetic()) { try { - theMethod = theMethod + methodToUse = methodToUse .getDeclaringClass() .getSuperclass() - .getMethod(theMethod.getName(), parameterTypes); + .getMethod(methodToUse.getName(), parameterTypes); parameterType = // LUKETODO: what to do here if anything? ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - theMethod, paramIndex); + methodToUse, paramIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" - + theMethod.getName() + "' does not exist for super class '" - + theMethod + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse .getDeclaringClass() .getSuperclass() + "'"); } @@ -618,9 +625,9 @@ public static List getResourceParameters( if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" - + theMethod.getName() + + methodToUse.getName() + "' in type '" - + theMethod + + methodToUse .getDeclaringClass() .getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); @@ -632,14 +639,14 @@ public static List getResourceParameters( Class newParameterType = elementDefinition.getImplementingClass(); if (!declaredParameterType.isAssignableFrom(newParameterType)) { throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + theMethod); + + operationParam.typeName() + "\" specified on method " + methodToUse); } parameterType = newParameterType; */ ourLog.info( "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - theMethod.getName(), + methodToUse.getName(), outerCollectionType, innerCollectionType, parameterType); @@ -654,15 +661,15 @@ public static List getResourceParameters( OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - - params.add(new OperationParameter( + ; + param = new OperationParameter( theContext, op.name(), operationParam.name(), operationParam.min(), operationParam.max(), description, - examples)); + examples); if (isNotBlank(operationParam.typeName())) { BaseRuntimeElementDefinition elementDefinition = theContext.getElementDefinition(operationParam.typeName()); @@ -677,7 +684,7 @@ public static List getResourceParameters( Class newParameterType = elementDefinition.getImplementingClass(); if (!declaredParameterType.isAssignableFrom(newParameterType)) { throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + theMethod); + + operationParam.typeName() + "\" specified on method " + methodToUse); } parameterType = newParameterType; } @@ -689,7 +696,7 @@ public static List getResourceParameters( } String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - params.add(new OperationParameter( + param = new OperationParameter( theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, @@ -716,7 +723,7 @@ public Object outgoingClient(Object theObject) { return ParametersUtil.createString( theContext, ((ValidationModeEnum) theObject).getCode()); } - })); + }); } else if (nextAnnotation instanceof Validate.Profile) { if (!parameterType.equals(String.class)) { throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" @@ -725,7 +732,7 @@ public Object outgoingClient(Object theObject) { } String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - params.add(new OperationParameter( + param = new OperationParameter( theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, @@ -743,42 +750,36 @@ public Object incomingServer(Object theObject) { public Object outgoingClient(Object theObject) { return ParametersUtil.createString(theContext, theObject.toString()); } - })); + }); } else { continue; } } } - if (params.isEmpty()) { + if (param == null) { throw new ConfigurationException( Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) - + " of method '" + theMethod.getName() + "' on type '" - + theMethod.getDeclaringClass().getCanonicalName() + + " of method '" + methodToUse.getName() + "' on type '" + + methodToUse.getDeclaringClass().getCanonicalName() + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } // LUKETODO: if we call this with an type with embedded params, we get an Exceptioon here // LUKETODO: Or do we expand the paramters here, and then foreaach parameters.add() ??? // ourLog.info("1234: param class: {}, method: {}", param.getClass().getCanonicalName(), - // theMethod.getName()); + // methodToUse.getName()); ourLog.info( "1234:about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - theMethod.getName(), + methodToUse.getName(), outerCollectionType, innerCollectionType, parameterType); - for (IParameter param : params) { - // LUKETODO: this may not work because of the multitude of values for each of the 3 last params for - // OperationEmbeddedParam - param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); - } - - parameters.addAll(params); + param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); + parameters.add(param); - // LUKETODO: what to do about this? paramIndex++; } return parameters; From dcbdc6b5d4658f3409ad34e51b88b8a8b6e9d538 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 10:52:15 -0500 Subject: [PATCH 21/75] Move closer to code reuse for MethodUtil but this isn't quite working yet. Old code still works. --- .../fhir/rest/server/method/MethodUtil.java | 65 ++++++++++++++----- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4901903cc661..10140f61a434 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -106,9 +106,8 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] @SuppressWarnings("unchecked") public static List getResourceParameters( final FhirContext theContext, final Method theMethod, Object theProvider) { - // This variable will be mutated so distinguish it from the argument to getResourceParameters() + // We mutate this variable so distinguish this from the argument to getResourceParameters Method methodToUse = theMethod; - ourLog.info("1234: getResourceParameters: " + methodToUse.getName()); List parameters = new ArrayList<>(); Class[] parameterTypes = methodToUse.getParameterTypes(); @@ -120,7 +119,7 @@ public static List getResourceParameters( methodToUse, OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty() - /*&& Year.now().equals(Year.of(1900))*/ + // && Year.now().equals(Year.of(1900)) ) { // disable for now ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); @@ -291,6 +290,8 @@ public static List getResourceParameters( for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { IParameter param = null; + // LUKETODO: final? + List paramContexts = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -652,6 +653,11 @@ public static List getResourceParameters( parameterType); // LUKETODO: how to handle multiple parameters.add(param); ????? + paramContexts.add(new ParamInitializationContext( + operationParameter, + parameterType, + outerCollectionTypeInner, + outerCollectionTypeInner)); } else { // some kind of Exception for now? } @@ -661,7 +667,7 @@ public static List getResourceParameters( OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - ; + param = new OperationParameter( theContext, op.name(), @@ -757,6 +763,12 @@ public Object outgoingClient(Object theObject) { } } + // LUKETODO: do we need this or just add conditional logic? + if (paramContexts.isEmpty()) { + paramContexts.add( + new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); + } + if (param == null) { throw new ConfigurationException( Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) @@ -765,23 +777,42 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } - // LUKETODO: if we call this with an type with embedded params, we get an Exceptioon here - // LUKETODO: Or do we expand the paramters here, and then foreaach parameters.add() ??? - // ourLog.info("1234: param class: {}, method: {}", param.getClass().getCanonicalName(), - // methodToUse.getName()); + for (ParamInitializationContext paramContext : paramContexts) { + ourLog.info( + "1234: NEW: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", + methodToUse.getName(), + paramContext.myOuterCollectionType, + paramContext.myInnerCollectionType, + paramContext.myParameterType); - ourLog.info( - "1234:about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - methodToUse.getName(), - outerCollectionType, - innerCollectionType, - parameterType); - - param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); - parameters.add(param); + paramContext.initialize(methodToUse); + parameters.add(paramContext.myParam); + } paramIndex++; } return parameters; } + + private static class ParamInitializationContext { + private IParameter myParam; + private Class myParameterType; + private Class> myOuterCollectionType = null; + private Class> myInnerCollectionType = null; + + private ParamInitializationContext( + IParameter myParam, + Class myParameterType, + Class> myOuterCollectionType, + Class> myInnerCollectionType) { + this.myParam = myParam; + this.myParameterType = myParameterType; + this.myOuterCollectionType = myOuterCollectionType; + this.myInnerCollectionType = myInnerCollectionType; + } + + void initialize(Method theMethod) { + myParam.initializeTypes(theMethod, myOuterCollectionType, myInnerCollectionType, myParameterType); + } + } } From 9a033cc18ed7cfdfae920081bb61b307be0eb31f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 11:46:44 -0500 Subject: [PATCH 22/75] Fix algorithm to deal with RequestDetails passed as part of parameters. --- ...seMethodBindingMethodParameterBuilder.java | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 94554ccbbe8b..b0b56aef66e4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -118,36 +118,38 @@ private static Constructor validateAndGetConstructor(Class theParameterTyp // LUKETODO: design for future use factory methods + // RequestDetails must be dealt with separately because there is no such concept in clinical-reasoning and the + // operation params classes must be defined in that project @Nonnull - private static Object[] buildMethodParamsInCorrectPositions( - Object[] theMethodParams, Object operationEmbeddedType) { + private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodParams, Object operationEmbeddedType) { - // LUKETODO: this is DUMB: extract the Request Details, then pass an enum of either FIRST OR LAST - final List requestDetailsIndexes = IntStream.range(0, theMethodParams.length) - .filter(index -> theMethodParams[index] instanceof RequestDetails) - .boxed() - .collect(Collectors.toUnmodifiableList()); - ; + final List requestDetailsMultiple = Arrays.stream(theMethodParams) + .filter(RequestDetails.class::isInstance) + .map(RequestDetails.class::cast) + .collect(Collectors.toUnmodifiableList()); - if (requestDetailsIndexes.size() > 1) { + if (requestDetailsMultiple.size() > 1) { throw new InternalErrorException( - Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); + Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); } - if (!requestDetailsIndexes.isEmpty()) { - final int requestDetailsIndex = requestDetailsIndexes.get(0); + if (requestDetailsMultiple.isEmpty()) { + // No RequestDetails at all + return new Object[] {operationEmbeddedType}; + } - if (requestDetailsIndex == 0) { - // RequestDetails goes first - return new Object[] {theMethodParams[0], operationEmbeddedType}; - } + final RequestDetails requestDetails = requestDetailsMultiple.get(0); + + final int indexOfRequestDetails = Arrays.asList(theMethodParams) + .indexOf(requestDetails); - // RequestDetails goes last - return new Object[] {operationEmbeddedType, theMethodParams[requestDetailsIndex]}; + if (indexOfRequestDetails == 0) { + // RequestDetails goes first + return new Object[] {requestDetails, operationEmbeddedType}; } - // No RequestDetails at all - return new Object[] {operationEmbeddedType}; + // RequestDetails goes last + return new Object[] {operationEmbeddedType, requestDetails}; } private static void validMethodParamTypes( From 5786be24b481caefc90fab3055ba7eec82f6ad6c Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 12:35:59 -0500 Subject: [PATCH 23/75] Fix new algorithm to work with evaluteMeasure but careGaps is still broken. --- .../fhir/rest/server/method/MethodUtil.java | 371 +++++++++--------- 1 file changed, 194 insertions(+), 177 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 10140f61a434..c0d26713a976 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -86,6 +86,10 @@ public class MethodUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class); + + // LUKETODO: this is TEMPORARY + private static final boolean IS_YEAR_1900 = Year.now().equals(Year.of(1900)); + private static final boolean IS_OLD_EMBEDDED_ALGO = ! IS_YEAR_1900; /** * Non instantiable */ @@ -119,7 +123,7 @@ public static List getResourceParameters( methodToUse, OperationEmbeddedParam.class); if (!operationEmbeddedTypes.isEmpty() - // && Year.now().equals(Year.of(1900)) + && IS_OLD_EMBEDDED_ALGO ) { // disable for now ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); @@ -377,6 +381,189 @@ public static List getResourceParameters( } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { + if (nextParameterAnnotations.length == 0) { + Operation op = methodToUse.getAnnotation(Operation.class); + if (op == null) { + throw new ConfigurationException(Msg.code(404) + + "@OperationParam detected on method that is not annotated with @Operation: " + + methodToUse.toGenericString()); + } + + // LUKETODO: try to combine into validateAndGet methods + final List> operationEmbeddedTypesInner = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + methodToUse, OperationEmbeddedParam.class); + + if (operationEmbeddedTypesInner.size() > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sOnly one type with embedded params is supported for now for method: %s", + Msg.code(9999927), methodToUse.getName())); + } + + if (!operationEmbeddedTypes.isEmpty() + && ! IS_OLD_EMBEDDED_ALGO) { // ensure this is the opposite so we can flip between the two + // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE + // METHOD!!!!!!!!!!!!!!!!!!!! + ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); + + final Class operationEmbeddedType = operationEmbeddedTypesInner.get(0); + + final Field[] fields = operationEmbeddedType.getDeclaredFields(); + + for (Field field : fields) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final Annotation[] fieldAnnotations = field.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), fieldName, methodToUse.getName())); + } + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, methodToUse.getName())); + } + + final Set annotationClassNames = Arrays.stream(fieldAnnotations) + .map(Annotation::annotationType) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); + + ourLog.info( + "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", + fieldName, + fieldType.getName(), + annotationClassNames); + + // This is the parameter on the field in question on the type with embedded params + // class: ex + // myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + parameters.add(new NullParameter()); + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + final OperationEmbeddedParam operationParam = + (OperationEmbeddedParam) fieldAnnotation; + + final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning + // repo + final OperationEmbeddedParameter operationParameter = + new OperationEmbeddedParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + + Class> outerCollectionTypeInner = null; + Class> innerCollectionTypeInner = null; + + parameterType = fieldType; + + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionTypeInner = + (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + if (parameterType == null + && methodToUse + .getDeclaringClass() + .isSynthetic()) { + try { + methodToUse = methodToUse + .getDeclaringClass() + .getSuperclass() + .getMethod(methodToUse.getName(), parameterTypes); + parameterType = + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter( + methodToUse, paramIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse + .getDeclaringClass() + .getSuperclass() + "'"); + } + } + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: now we're processing the generic parameter, so capture the inner and + // outer + // types + // Collection + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionTypeInner = innerCollectionTypeInner; + innerCollectionTypeInner = + (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: as a guard: if this is still a Collection, then throw because + // something went + // wrong + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException( + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + + methodToUse.getName() + + "' in type '" + + methodToUse + .getDeclaringClass() + .getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + // LUKETODO: do I need to worry about this: + /* + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + methodToUse); + } + parameterType = newParameterType; + */ + +// ourLog.info( +// "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", +// methodToUse.getName(), +// outerCollectionType, +// innerCollectionType, +// parameterType); + + // LUKETODO: how to handle multiple parameters.add(param); ????? + paramContexts.add(new ParamInitializationContext( + operationParameter, + parameterType, + outerCollectionTypeInner, + outerCollectionTypeInner)); + // LUKETODO: nasty hack to skip the null check + param = operationParameter; + } else { + // some kind of Exception for now? + } + } + } + } + for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; @@ -493,176 +680,6 @@ public static List getResourceParameters( + methodToUse.toGenericString()); } - // LUKETODO: try to combine into validateAndGet methods - final List> operationEmbeddedTypesInner = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); - - if (operationEmbeddedTypesInner.size() > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - - if (!operationEmbeddedTypes.isEmpty() && Year.now().equals(Year.of(1900))) { - // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE - // METHOD!!!!!!!!!!!!!!!!!!!! - ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); - - final Class operationEmbeddedType = operationEmbeddedTypesInner.get(0); - - final Field[] fields = operationEmbeddedType.getDeclaredFields(); - - for (Field field : fields) { - final String fieldName = field.getName(); - final Class fieldType = field.getType(); - final Annotation[] fieldAnnotations = field.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, methodToUse.getName())); - } - - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, methodToUse.getName())); - } - - final Set annotationClassNames = Arrays.stream(fieldAnnotations) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(Collectors.toUnmodifiableSet()); - - ourLog.info( - "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", - fieldName, - fieldType.getName(), - annotationClassNames); - - // This is the parameter on the field in question on the type with embedded params - // class: ex - // myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; - - if (fieldAnnotation instanceof IdParam) { - // skip - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - final OperationEmbeddedParam operationParam = - (OperationEmbeddedParam) fieldAnnotation; - - final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning - // repo - final OperationEmbeddedParameter operationParameter = - new OperationEmbeddedParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - - Class> outerCollectionTypeInner = null; - Class> innerCollectionTypeInner = null; - - parameterType = fieldType; - - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionTypeInner = - (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); - if (parameterType == null - && methodToUse - .getDeclaringClass() - .isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), parameterTypes); - parameterType = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - methodToUse, paramIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse - .getDeclaringClass() - .getSuperclass() + "'"); - } - } - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: now we're processing the generic parameter, so capture the inner and - // outer - // types - // Collection - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionTypeInner = innerCollectionTypeInner; - innerCollectionTypeInner = - (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: as a guard: if this is still a Collection, then throw because - // something went - // wrong - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse - .getDeclaringClass() - .getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - // LUKETODO: do I need to worry about this: - /* - - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ - - ourLog.info( - "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - methodToUse.getName(), - outerCollectionType, - innerCollectionType, - parameterType); - - // LUKETODO: how to handle multiple parameters.add(param); ????? - paramContexts.add(new ParamInitializationContext( - operationParameter, - parameterType, - outerCollectionTypeInner, - outerCollectionTypeInner)); - } else { - // some kind of Exception for now? - } - } - } OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); @@ -778,12 +795,12 @@ public Object outgoingClient(Object theObject) { } for (ParamInitializationContext paramContext : paramContexts) { - ourLog.info( - "1234: NEW: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - methodToUse.getName(), - paramContext.myOuterCollectionType, - paramContext.myInnerCollectionType, - paramContext.myParameterType); +// ourLog.info( +// "1234: NEW: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", +// methodToUse.getName(), +// paramContext.myOuterCollectionType, +// paramContext.myInnerCollectionType, +// paramContext.myParameterType); paramContext.initialize(methodToUse); parameters.add(paramContext.myParam); From c03e0219b9f959488bcf606203c4f546f7b9c2ac Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 13:18:27 -0500 Subject: [PATCH 24/75] Get rid of use of OperationParams. Fix new algo so it passes startup. Still need to fix Collection MethodUtil code as it doesn't work at request time. --- .../BaseMethodBindingMethodParameterBuilder.java | 11 +++++++++++ .../ca/uhn/fhir/rest/server/method/MethodUtil.java | 10 ++++------ .../fhir/cr/r4/measure/CareGapsOperationProvider.java | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index b0b56aef66e4..4b7cf78480ce 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -84,6 +84,17 @@ private static Object buildOperationEmbeddedObject( validMethodParamTypes(methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); + ourLog.info("constructor args: \n{}\nand non-request details parameter args: \n{}\n and orig method params:\n{}", + Arrays.toString(constructor.getParameterTypes()), + Arrays.toString(methodParamsWithoutRequestDetails), + Arrays.toString(theMethodParams)); + + if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { + throw new InternalErrorException(String.format("1234: mismatch between constructor args: %s and non-request details parameter args: %s", + Arrays.toString(constructor.getParameterTypes()), + Arrays.toString(methodParamsWithoutRequestDetails))); + } + return constructor.newInstance(methodParamsWithoutRequestDetails); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index c0d26713a976..16f5384b2338 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -206,6 +206,7 @@ public static List getResourceParameters( parameterType = fieldType; + // LUKETODO: this is broken: we are LOSING the generic type for some reason if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; // LUKETODO: come up with another method to do this for field params @@ -229,12 +230,7 @@ public static List getResourceParameters( .getSuperclass() + "'"); } } - // LUKETODO: - // declaredParameterType = parameterType; } - // LUKETODO: now we're processing the generic parameter, so capture the inner and outer - // types - // Collection // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; @@ -369,6 +365,7 @@ public static List getResourceParameters( param = new ServletResponseParameter(); } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { + // LUKETODO: this is where I got nailed param = new RequestDetailsParameter(); } else if (parameterType.equals(IInterceptorBroadcaster.class)) { param = new InterceptorBroadcasterParameter(); @@ -781,7 +778,8 @@ public Object outgoingClient(Object theObject) { } // LUKETODO: do we need this or just add conditional logic? - if (paramContexts.isEmpty()) { + if (paramContexts.isEmpty() + || ! (param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add RequestDetails paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 7a4b9a53e844..acd89070154b 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -97,8 +97,8 @@ public CareGapsOperationProvider( "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) public Parameters careGapsReport( - // LUKETODO: include RequestDetails in Params object? - RequestDetails theRequestDetails, @OperationParam(name = "params") CareGapsParams theParams) { + // LUKETODO: do NOT use @OperationParam if this is for embedded params and document this + RequestDetails theRequestDetails, CareGapsParams theParams) { return myR4CareGapsProcessorFactory .create(theRequestDetails) From c964f0d04482e77cf4d78f24a432cb91077f675f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 13:33:22 -0500 Subject: [PATCH 25/75] Fix bug with collection types. Kill old code. --- .../fhir/rest/server/method/MethodUtil.java | 193 +----------------- 1 file changed, 11 insertions(+), 182 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 16f5384b2338..09f59caeabb0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -89,7 +89,7 @@ public class MethodUtil { // LUKETODO: this is TEMPORARY private static final boolean IS_YEAR_1900 = Year.now().equals(Year.of(1900)); - private static final boolean IS_OLD_EMBEDDED_ALGO = ! IS_YEAR_1900; + private static final boolean IS_OLD_EMBEDDED_ALGO = IS_YEAR_1900; /** * Non instantiable */ @@ -116,182 +116,10 @@ public static List getResourceParameters( Class[] parameterTypes = methodToUse.getParameterTypes(); int paramIndex = 0; - // LUKETODO: one param per method parameter: what happens if we expand this? - - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); - - if (!operationEmbeddedTypes.isEmpty() - && IS_OLD_EMBEDDED_ALGO - ) { // disable for now - ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); - - // This is the @Operation parameter on the method itself (ex: evaluateMeasure) - final Operation op = methodToUse.getAnnotation(Operation.class); - - if (operationEmbeddedTypes.size() > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - - // LUKETODO: handle multiple RequestDetails with an error - - for (Class parameterType : parameterTypes) { - // If either the first or second parameter is a RequestDetails, handle it - if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { - parameters.add(new RequestDetailsParameter()); - } else { // LUKETODO: specific check here? - // LUKETODO: limit to a single Params object - final Field[] fields = parameterType.getDeclaredFields(); - - for (Field field : fields) { - final String fieldName = field.getName(); - final Class fieldType = field.getType(); - final Annotation[] fieldAnnotations = field.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, methodToUse.getName())); - } - - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, methodToUse.getName())); - } - - final Set annotationClassNames = Arrays.stream(fieldAnnotations) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(Collectors.toUnmodifiableSet()); - - ourLog.info( - "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", - fieldName, - fieldType.getName(), - annotationClassNames); - - // This is the parameter on the field in question on the type with embedded params class: ex - // myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; - - // LUKETODO: what if this is not a IdParam or an OperationParam? - if (fieldAnnotation instanceof IdParam) { - parameters.add(new NullParameter()); - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - final OperationEmbeddedParam operationParam = (OperationEmbeddedParam) fieldAnnotation; - - final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning repo - final OperationEmbeddedParameter operationParameter = new OperationEmbeddedParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - parameterType = fieldType; - - // LUKETODO: this is broken: we are LOSING the generic type for some reason - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); - if (parameterType == null - && methodToUse.getDeclaringClass().isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), parameterTypes); - parameterType = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - methodToUse, paramIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse - .getDeclaringClass() - .getSuperclass() + "'"); - } - } - } - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: as a guard: if this is still a Collection, then throw because something went - // wrong - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse - .getDeclaringClass() - .getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - // LUKETODO: do I need to worry about this: - /* - - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ - - ourLog.info( - "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", - methodToUse.getName(), - outerCollectionType, - innerCollectionType, - parameterType); - - operationParameter.initializeTypes( - methodToUse, outerCollectionType, innerCollectionType, parameterType); - - parameters.add(operationParameter); - } else { - throw new ConfigurationException( - Msg.code(999995) + "Unsupported param fieldType: " + fieldAnnotation); - } - } - } - } - - // LUKETODO: short-circuit for now - return parameters; - } for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { IParameter param = null; - // LUKETODO: final? - List paramContexts = new ArrayList<>(); + final List paramContexts = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -320,9 +148,7 @@ public static List getResourceParameters( } declaredParameterType = parameterType; } - // LUKETODO: now we're processing the generic parameter, so capture the inner and outer types - // Collection - // LUKETODO: using reflection, find the + if (Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; innerCollectionType = (Class>) parameterType; @@ -379,7 +205,12 @@ public static List getResourceParameters( param = new SearchTotalModeParameter(); } else { if (nextParameterAnnotations.length == 0) { - Operation op = methodToUse.getAnnotation(Operation.class); + // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + methodToUse, OperationEmbeddedParam.class); + + final Operation op = methodToUse.getAnnotation(Operation.class); + if (op == null) { throw new ConfigurationException(Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: " @@ -476,9 +307,7 @@ public static List getResourceParameters( // LUKETODO: come up with another method to do this for field params parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); if (parameterType == null - && methodToUse - .getDeclaringClass() - .isSynthetic()) { + && methodToUse.getDeclaringClass().isSynthetic()) { try { methodToUse = methodToUse .getDeclaringClass() @@ -551,7 +380,7 @@ public static List getResourceParameters( operationParameter, parameterType, outerCollectionTypeInner, - outerCollectionTypeInner)); + innerCollectionTypeInner)); // LUKETODO: nasty hack to skip the null check param = operationParameter; } else { From c29f0b4156ceb3fc5d452654e28b1cf640b807f5 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 16:04:37 -0500 Subject: [PATCH 26/75] Fix bug with collection types. Kill old code. Add basic unit tests. --- .../fhir/rest/server/method/MethodUtil.java | 146 ++++++-------- .../method/ParamInitializationContext.java | 31 +++ ...thodBindingMethodParameterBuilderTest.java | 114 +++++++++++ .../rest/server/method/MethodUtilTest.java | 180 +++++++++++++++++- 4 files changed, 384 insertions(+), 87 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 09f59caeabb0..29d3853d6dea 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -63,6 +63,7 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -71,7 +72,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.time.Year; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -86,10 +86,6 @@ public class MethodUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class); - - // LUKETODO: this is TEMPORARY - private static final boolean IS_YEAR_1900 = Year.now().equals(Year.of(1900)); - private static final boolean IS_OLD_EMBEDDED_ALGO = IS_YEAR_1900; /** * Non instantiable */ @@ -118,6 +114,7 @@ public static List getResourceParameters( int paramIndex = 0; for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { + // LUKETODO: wrapper object for all of these IParameter param = null; final List paramContexts = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; @@ -191,7 +188,6 @@ public static List getResourceParameters( param = new ServletResponseParameter(); } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { - // LUKETODO: this is where I got nailed param = new RequestDetailsParameter(); } else if (parameterType.equals(IInterceptorBroadcaster.class)) { param = new InterceptorBroadcasterParameter(); @@ -204,13 +200,14 @@ public static List getResourceParameters( } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { + // LUKETODO: introduce new state objects + final Operation op = methodToUse.getAnnotation(Operation.class); + // LUKETODO: this relies on new new embedded params explicitly leave OUT @OperationParam on parameters if (nextParameterAnnotations.length == 0) { // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( methodToUse, OperationEmbeddedParam.class); - final Operation op = methodToUse.getAnnotation(Operation.class); - if (op == null) { throw new ConfigurationException(Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: " @@ -229,8 +226,8 @@ public static List getResourceParameters( Msg.code(9999927), methodToUse.getName())); } - if (!operationEmbeddedTypes.isEmpty() - && ! IS_OLD_EMBEDDED_ALGO) { // ensure this is the opposite so we can flip between the two + // LUKETODO: else???? + if (!operationEmbeddedTypes.isEmpty()) { // ensure this is the opposite so we can flip between the two // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE // METHOD!!!!!!!!!!!!!!!!!!!! ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); @@ -276,44 +273,31 @@ public static List getResourceParameters( if (fieldAnnotation instanceof IdParam) { parameters.add(new NullParameter()); } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - final OperationEmbeddedParam operationParam = - (OperationEmbeddedParam) fieldAnnotation; - - final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning - // repo - final OperationEmbeddedParameter operationParameter = - new OperationEmbeddedParameter( + final OperationEmbeddedParameter operationEmbeddedParameter = + getOperationEmbeddedParameter( theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); + fieldAnnotation, + op, + (OperationEmbeddedParam) fieldAnnotation); + + parameterType = fieldType; + Class parameterTypeInner = parameterType; Class> outerCollectionTypeInner = null; Class> innerCollectionTypeInner = null; - parameterType = fieldType; - if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionTypeInner = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); - if (parameterType == null + parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); + if (parameterTypeInner == null && methodToUse.getDeclaringClass().isSynthetic()) { try { methodToUse = methodToUse .getDeclaringClass() .getSuperclass() .getMethod(methodToUse.getName(), parameterTypes); - parameterType = + parameterTypeInner = // LUKETODO: what to do here if anything? ReflectionUtil.getGenericCollectionTypeOfMethodParameter( methodToUse, paramIndex); @@ -333,12 +317,12 @@ public static List getResourceParameters( // types // Collection // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { + if (Collection.class.isAssignableFrom(parameterTypeInner)) { outerCollectionTypeInner = innerCollectionTypeInner; innerCollectionTypeInner = (Class>) parameterType; // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(field); + parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); // LUKETODO: // declaredParameterType = parameterType; } @@ -346,7 +330,7 @@ public static List getResourceParameters( // something went // wrong // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { + if (Collection.class.isAssignableFrom(parameterTypeInner)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() @@ -377,12 +361,12 @@ public static List getResourceParameters( // LUKETODO: how to handle multiple parameters.add(param); ????? paramContexts.add(new ParamInitializationContext( - operationParameter, - parameterType, + operationEmbeddedParameter, + parameterTypeInner, outerCollectionTypeInner, innerCollectionTypeInner)); // LUKETODO: nasty hack to skip the null check - param = operationParameter; + param = operationEmbeddedParameter; } else { // some kind of Exception for now? } @@ -499,7 +483,6 @@ public static List getResourceParameters( } else if (nextAnnotation instanceof ConditionalUrlParam) { param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); } else if (nextAnnotation instanceof OperationParam) { - Operation op = methodToUse.getAnnotation(Operation.class); if (op == null) { throw new ConfigurationException(Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: " @@ -573,22 +556,23 @@ public Object outgoingClient(Object theObject) { theContext, ((ValidationModeEnum) theObject).getCode()); } }); - } else if (nextAnnotation instanceof Validate.Profile) { - if (!parameterType.equals(String.class)) { - throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" + } else { + if (nextAnnotation instanceof Validate.Profile) { + if (!parameterType.equals(String.class)) { + throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); - } - String description = ParametersUtil.extractDescription(nextParameterAnnotations); - List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_PROFILE, - 0, - 1, - description, - examples) + } + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List examples = ParametersUtil.extractExamples(nextParameterAnnotations); + param = new OperationParameter( + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_PROFILE, + 0, + 1, + description, + examples) .setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { @@ -600,15 +584,14 @@ public Object outgoingClient(Object theObject) { return ParametersUtil.createString(theContext, theObject.toString()); } }); - } else { - continue; + } } } } // LUKETODO: do we need this or just add conditional logic? if (paramContexts.isEmpty() - || ! (param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add RequestDetails + || ! (param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add RequestDetails if it's last paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } @@ -622,15 +605,8 @@ public Object outgoingClient(Object theObject) { } for (ParamInitializationContext paramContext : paramContexts) { -// ourLog.info( -// "1234: NEW: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", -// methodToUse.getName(), -// paramContext.myOuterCollectionType, -// paramContext.myInnerCollectionType, -// paramContext.myParameterType); - paramContext.initialize(methodToUse); - parameters.add(paramContext.myParam); + parameters.add(paramContext.getParam()); } paramIndex++; @@ -638,25 +614,23 @@ public Object outgoingClient(Object theObject) { return parameters; } - private static class ParamInitializationContext { - private IParameter myParam; - private Class myParameterType; - private Class> myOuterCollectionType = null; - private Class> myInnerCollectionType = null; - - private ParamInitializationContext( - IParameter myParam, - Class myParameterType, - Class> myOuterCollectionType, - Class> myInnerCollectionType) { - this.myParam = myParam; - this.myParameterType = myParameterType; - this.myOuterCollectionType = myOuterCollectionType; - this.myInnerCollectionType = myInnerCollectionType; - } - - void initialize(Method theMethod) { - myParam.initializeTypes(theMethod, myOuterCollectionType, myInnerCollectionType, myParameterType); - } + @Nonnull + private static OperationEmbeddedParameter getOperationEmbeddedParameter(FhirContext theContext, Annotation fieldAnnotation, Operation op, OperationEmbeddedParam operationParam) { + final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning + // repo + return new OperationEmbeddedParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java new file mode 100644 index 000000000000..60bec09edc13 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.server.method; + +import java.lang.reflect.Method; +import java.util.Collection; + +// LUKETODO: javadoc +class ParamInitializationContext { + private final IParameter myParam; + private final Class myParameterType; + private final Class> myOuterCollectionType; + private final Class> myInnerCollectionType; + + ParamInitializationContext( + IParameter theParam, + Class theParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType) { + myParam = theParam; + myParameterType = theParameterType; + myOuterCollectionType = theOuterCollectionType; + myInnerCollectionType = theInnerCollectionType; + } + + public IParameter getParam() { + return myParam; + } + + void initialize(Method theMethod) { + myParam.initializeTypes(theMethod, myOuterCollectionType, myInnerCollectionType, myParameterType); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java new file mode 100644 index 000000000000..13aaae18f830 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -0,0 +1,114 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +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.annotation.Annotation; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BaseMethodBindingMethodParameterBuilderTest { + + @Mock + private Method myMethod; + + @Mock + private Annotation myAnnotation; + + @Test + void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { + InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, new Object[]{"param1"}); + }); + + assertTrue(exception.getMessage().contains("Method cannot be null")); + } + + @Test + void buildMethodParams_withNullParameterTypes_shouldThrowInternalErrorException() throws Exception { + when(myMethod.getParameterTypes()).thenReturn(null); + when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{{}}); + + InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); + }); + + assertTrue(exception.getMessage().contains("Parameter types cannot be null")); + } + + @Test + void buildMethodParams_withNullParameterAnnotations_shouldThrowInternalErrorException() throws Exception { + when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); + when(myMethod.getParameterAnnotations()).thenReturn(null); + + InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); + }); + + assertTrue(exception.getMessage().contains("Parameter annotations cannot be null")); + } + + @Test + void buildMethodParams_withEmptyParameterTypes_shouldReturnEmptyArray() throws Exception { + when(myMethod.getParameterTypes()).thenReturn(new Class[]{}); + when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{{}}); + + Object[] result = BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{}); + + assertArrayEquals(new Object[]{}, result); + } + + @Test + void buildMethodParams_withSingleOperationEmbeddedParam_shouldReturnCorrectParams() throws Exception { + when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); + final Annotation[][] doubleArray = new Annotation[][] {{myAnnotation}}; +// when(myMethod.getParameterAnnotations()).thenReturn(doubleArray); +// when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{ +// {new OperationEmbeddedParam() { +// @Override +// public String name() { +// return ""; +// } +// +// @Override +// public Class type() { +// return null; +// } +// +// @Override +// public String typeName() { +// return ""; +// } +// +// @Override +// public int min() { +// return 0; +// } +// +// @Override +// public int max() { +// return 0; +// } +// +// @Override +// public Class annotationType() { +// return OperationEmbeddedParam.class; +// } +// }} +// }); + + Object[] result = BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); + + assertEquals(1, result.length); + assertTrue(result[0] instanceof String); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 8028f0fe1479..49d2cfede3c1 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -1,7 +1,185 @@ package ca.uhn.fhir.rest.server.method; -import static org.junit.jupiter.api.Assertions.*; +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import org.hl7.fhir.instance.model.api.IBaseResource; +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.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) class MethodUtilTest { + @Mock + private FhirContext myFhirContext; + + @Mock + private Method method; + + @Mock + private Object provider; + + void sampleMethod(@RequiredParam(name = "param") String param) { + // Sample method for testing + } + + @Test + void getResourceParameters_withOptionalParam_shouldReturnSearchParameter() throws NoSuchMethodException { + Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); + when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new OptionalParam() { + @Override + public Class annotationType() { + return OptionalParam.class; + } + + @Override + public String[] chainBlacklist() { + return new String[0]; + } + + @Override + public String[] chainWhitelist() { + return new String[0]; + } + + @Override + public Class[] compositeTypes() { + return new Class[0]; + } + + @Override + public String name() { + return "param"; + } + + @Override + public Class[] targetTypes() { + return new Class[0]; + } + }}}); + when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); + + List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); + + assertEquals(1, parameters.size()); + assertInstanceOf(SearchParameter.class, parameters.get(0)); + SearchParameter searchParameter = (SearchParameter) parameters.get(0); + assertEquals("param", searchParameter.getName()); + assertFalse(searchParameter.isRequired()); + } + + @Test + void getResourceParameters_withInvalidAnnotation_shouldThrowConfigurationException() throws NoSuchMethodException { + Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); + when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new Annotation() { + @Override + public Class annotationType() { + return Annotation.class; + } + }}}); + when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + MethodUtil.getResourceParameters(myFhirContext, method, provider); + }); + + assertTrue(exception.getMessage().contains("has no recognized FHIR interface parameter nextParameterAnnotations")); + } + + @Test + void getResourceParameters_withMultipleAnnotations_shouldReturnCorrectParameters() throws NoSuchMethodException { + Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); + when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{ + {new RequiredParam() { + @Override + public Class annotationType() { + return RequiredParam.class; + } + + @Override + public String[] chainBlacklist() { + return new String[0]; + } + + @Override + public String[] chainWhitelist() { + return new String[0]; + } + + @Override + public Class[] compositeTypes() { + return new Class[0]; + } + + @Override + public String name() { + return "param1"; + } + + @Override + public Class[] targetTypes() { + return new Class[0]; + } + }}, + {new OptionalParam() { + @Override + public Class annotationType() { + return OptionalParam.class; + } + + @Override + public String[] chainBlacklist() { + return new String[0]; + } + + @Override + public String[] chainWhitelist() { + return new String[0]; + } + + @Override + public Class[] compositeTypes() { + return new Class[0]; + } + + @Override + public String name() { + return "param2"; + } + + @Override + public Class[] targetTypes() { + return new Class[0]; + } + }} + }); + when(method.getParameterTypes()).thenReturn(new Class[]{String.class, String.class}); + + List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); + + assertEquals(2, parameters.size()); + assertTrue(parameters.get(0) instanceof SearchParameter); + assertTrue(parameters.get(1) instanceof SearchParameter); + SearchParameter searchParameter1 = (SearchParameter) parameters.get(0); + SearchParameter searchParameter2 = (SearchParameter) parameters.get(1); + assertEquals("param1", searchParameter1.getName()); + assertTrue(searchParameter1.isRequired()); + assertEquals("param2", searchParameter2.getName()); + assertFalse(searchParameter2.isRequired()); + } } From e8237783d2c75f578020129e79548aa3acfd829e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 17:44:40 -0500 Subject: [PATCH 27/75] Write more tests and improve production class. --- ...seMethodBindingMethodParameterBuilder.java | 47 ++- ...thodBindingMethodParameterBuilderTest.java | 335 ++++++++++++++---- 2 files changed, 291 insertions(+), 91 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 4b7cf78480ce..01df2e3a465a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import java.util.stream.IntStream; import static java.util.function.Predicate.not; @@ -26,8 +25,25 @@ class BaseMethodBindingMethodParameterBuilder { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + private BaseMethodBindingMethodParameterBuilder() {} + static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + + if (theMethod == null || theMethodParams == null) { + throw new InternalErrorException(String.format("%s Either theMethod: %s or theMethodParams: %s is null", + Msg.code(234198927), theMethod, Arrays.toString(theMethodParams))); + } + + final Class[] methodParameterTypes = theMethod.getParameterTypes(); + + if (Arrays.stream(methodParameterTypes) + .filter(RequestDetails.class::isAssignableFrom).count() > 1) { + throw new InternalErrorException(String.format( + "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", + Msg.code(924469635), theMethod.getName())); + } + final List> parameterTypesWithOperationEmbeddedParam = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( theMethod, OperationEmbeddedParam.class); @@ -42,10 +58,20 @@ static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) return theMethodParams; } - if (theMethodParams.length > 2 && Arrays.stream(theMethodParams).noneMatch(RequestDetails.class::isInstance)) { + final long numRequestDetails = Arrays.stream(methodParameterTypes) + .filter(RequestDetails.class::isAssignableFrom) + .count(); + + if (numRequestDetails == 0 && methodParameterTypes.length > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and one must be a RequestDetails: %s", - Msg.code(924469634), theMethod.getName())); + "%s1234: Invalid operation with embedded parameters. Cannot have more than 1 params and no RequestDetails: %s", + Msg.code(924469634), theMethod.getName())); + } + + if (numRequestDetails > 0 && methodParameterTypes.length > 2) { + throw new InternalErrorException(String.format( + "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and a RequestDetails: %s", + Msg.code(924469634), theMethod.getName())); } final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); @@ -84,11 +110,6 @@ private static Object buildOperationEmbeddedObject( validMethodParamTypes(methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); - ourLog.info("constructor args: \n{}\nand non-request details parameter args: \n{}\n and orig method params:\n{}", - Arrays.toString(constructor.getParameterTypes()), - Arrays.toString(methodParamsWithoutRequestDetails), - Arrays.toString(theMethodParams)); - if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { throw new InternalErrorException(String.format("1234: mismatch between constructor args: %s and non-request details parameter args: %s", Arrays.toString(constructor.getParameterTypes()), @@ -183,11 +204,6 @@ private static void validateMethodParamType(Object methodParamAtIndex, Class final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); - ourLog.info( - "1234: methodParamClassAtIndex: {}, parameterClassAtIndex: {}", - methodParamClassAtIndex, - parameterClassAtIndex); - // LUKETODO: fix this this is gross if (Collection.class.isAssignableFrom(methodParamClassAtIndex) || Collection.class.isAssignableFrom(parameterClassAtIndex)) { @@ -197,7 +213,8 @@ private static void validateMethodParamType(Object methodParamAtIndex, Class "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex)); } - } else if (methodParamClassAtIndex != parameterClassAtIndex) { + // Ex: Field is declared as an IIdType, but argument is an IdDt + } else if (! parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { throw new InternalErrorException(String.format( "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 13aaae18f830..3d390097a17c 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -1,114 +1,297 @@ package ca.uhn.fhir.rest.server.method; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.jupiter.api.Disabled; 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.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) class BaseMethodBindingMethodParameterBuilderTest { - @Mock - private Method myMethod; + private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); - @Mock - private Annotation myAnnotation; + private void superSimple() {} + + private void sampleMethodOperationParams( + @IdParam IIdType theIdType, + @OperationParam(name = "param1") String theParam1, + @OperationEmbeddedParam(name = "param2") Integer theParam2, + @OperationEmbeddedParam(name = "param3") List theParam3) { + // Sample method for testing + } + + private static class SampleParams { + @OperationEmbeddedParam(name = "param1") + private final String myParam1; + + @OperationEmbeddedParam(name = "param2") + private final Integer myParam2; + + @OperationEmbeddedParam(name = "param3") + private final List myParam3; + + public SampleParams(String myParam1, Integer myParam2, List myParam3) { + this.myParam1 = myParam1; + this.myParam2 = myParam2; + this.myParam3 = myParam3; + } + + public String getParam1() { + return myParam1; + } + + public Integer getParam2() { + return myParam2; + } + + public List getParam3() { + return myParam3; + } + + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SampleParams that = (SampleParams) o; + return Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2) && Objects.equals(myParam3, that.myParam3); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2, myParam3); + } + + @Override + public String toString() { + return new StringJoiner(", ", SampleParams.class.getSimpleName() + "[", "]") + .add("myParam1='" + myParam1 + "'") + .add("myParam2=" + myParam2) + .add("myParam3=" + myParam3) + .toString(); + } + } + + private static class SampleParamsWithIdParam { + @IdParam + private final IIdType myId; + + @OperationEmbeddedParam(name = "param1") + private final String myParam1; + + @OperationEmbeddedParam(name = "param2") + private final Integer myParam2; + + @OperationEmbeddedParam(name = "param3") + private final List myParam3; + + public SampleParamsWithIdParam(IIdType myId, String myParam1, Integer myParam2, List myParam3) { + this.myId = myId; + this.myParam1 = myParam1; + this.myParam2 = myParam2; + this.myParam3 = myParam3; + } + + public IIdType getId() { + return myId; + } + + public String getParam1() { + return myParam1; + } + + public Integer getParam2() { + return myParam2; + } + + public List getParam3() { + return myParam3; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SampleParamsWithIdParam that = (SampleParamsWithIdParam) o; + return Objects.equals(myId, that.myId) && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2) && Objects.equals(myParam3, that.myParam3); + } + + @Override + public int hashCode() { + return Objects.hash(myId, myParam1, myParam2, myParam3); + } + + @Override + public String toString() { + return new StringJoiner(", ", SampleParamsWithIdParam.class.getSimpleName() + "[", "]") + .add("myId=" + myId) + .add("myParam1='" + myParam1 + "'") + .add("myParam2=" + myParam2) + .add("myParam3=" + myParam3) + .toString(); + } + } + + private String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, SampleParams theParams) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeRequestDetailsLast(SampleParams theParams, RequestDetails theRequestDetails) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeNoRequestDetails(SampleParams theParams) { + // return something arbitrary + return theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, SampleParams theParams, RequestDetails theRequestDetails2) { + // return something arbitrary + return theRequestDetails1.getId().getValue() + theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, SampleParamsWithIdParam theParams) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + private String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParams theParams) { + // return something arbitrary + return theParams.getParam1(); + } + + // LUKETODO: wrong params + // LUKETODO: wrong param order + // LUKETODO: RequestDetails passed but not in signature + // LUKETODO: RequestDetails in signature but not passed @Test - void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { - InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, new Object[]{"param1"}); - }); + void happyPathOperationParamsEmptyParams() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = this.getClass().getDeclaredMethod("superSimple"); + final Object[] inputParams = new Object[] {}; + + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); - assertTrue(exception.getMessage().contains("Method cannot be null")); + assertArrayEquals(inputParams, actualOutputParams); } @Test - void buildMethodParams_withNullParameterTypes_shouldThrowInternalErrorException() throws Exception { - when(myMethod.getParameterTypes()).thenReturn(null); - when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{{}}); + void happyPathOperationParamsNonEmptyParams() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodOperationParams", IIdType.class, String.class, Integer.class, List.class); + final Object[] inputParams = new Object[] {new IdDt(), "param1", 2, List.of("param3")}; - InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); - }); + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); - assertTrue(exception.getMessage().contains("Parameter types cannot be null")); + assertArrayEquals(inputParams, actualOutputParams); } @Test - void buildMethodParams_withNullParameterAnnotations_shouldThrowInternalErrorException() throws Exception { - when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); - when(myMethod.getParameterAnnotations()).thenReturn(null); + void happyPathOperationEmbeddedTypesNoRequestDetails() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeNoRequestDetails", SampleParams.class); + final Object[] inputParams = new Object[] {"param1", 2, List.of("param3")}; + final Object[] expectedOutputParams = new Object[] {new SampleParams("param1", 2, List.of("param3"))}; - InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); - }); + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); - assertTrue(exception.getMessage().contains("Parameter annotations cannot be null")); + assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test - void buildMethodParams_withEmptyParameterTypes_shouldReturnEmptyArray() throws Exception { - when(myMethod.getParameterTypes()).thenReturn(new Class[]{}); - when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{{}}); + void happyPathOperationEmbeddedTypesRequestDetailsFirst() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsFirst", RequestDetails.class, SampleParams.class); + final Object[] inputParams = new Object[] {REQUEST_DETAILS, "param1", 2, List.of("param3")}; + final Object[] expectedOutputParams = new Object[] {REQUEST_DETAILS, new SampleParams("param1", 2, List.of("param3"))}; - Object[] result = BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{}); + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); - assertArrayEquals(new Object[]{}, result); + assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test - void buildMethodParams_withSingleOperationEmbeddedParam_shouldReturnCorrectParams() throws Exception { - when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); - final Annotation[][] doubleArray = new Annotation[][] {{myAnnotation}}; -// when(myMethod.getParameterAnnotations()).thenReturn(doubleArray); -// when(myMethod.getParameterAnnotations()).thenReturn(new Annotation[][]{ -// {new OperationEmbeddedParam() { -// @Override -// public String name() { -// return ""; -// } -// -// @Override -// public Class type() { -// return null; -// } -// -// @Override -// public String typeName() { -// return ""; -// } -// -// @Override -// public int min() { -// return 0; -// } -// -// @Override -// public int max() { -// return 0; -// } -// -// @Override -// public Class annotationType() { -// return OperationEmbeddedParam.class; -// } -// }} -// }); + void happyPathOperationEmbeddedTypesRequestDetailsLast() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsLast", SampleParams.class, RequestDetails.class); + final Object[] inputParams = new Object[] {"param1", 2, List.of("param3"), REQUEST_DETAILS}; + final Object[] expectedOutputParams = new Object[] {new SampleParams("param1", 2, List.of("param3")), REQUEST_DETAILS}; + + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + @Test + void happyPathOperationEmbeddedTypesWithIdType() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + final IdDt id = new IdDt(); + final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType", RequestDetails.class, SampleParamsWithIdParam.class); + final Object[] inputParams = new Object[] {REQUEST_DETAILS, id, "param1", 2, List.of("param3")}; + final Object[] expectedOutputParams = new Object[] {REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", 2, List.of("param3")), }; + + final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + @Test + void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { + assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, new Object[]{}); + }); + } - Object[] result = BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); + @Test + void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws NoSuchMethodException { + final Method sampleMethod = this.getClass().getDeclaredMethod("superSimple"); + + assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, null); + }); + } + + @Test + void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException() throws NoSuchMethodException { + assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, null); + }); + } - assertEquals(1, result.length); - assertTrue(result[0] instanceof String); + @Test + void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() throws NoSuchMethodException { + final Method method = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeMultipleRequestDetails", RequestDetails.class, SampleParams.class, RequestDetails.class); + final Object[] inputParams = new Object[] {REQUEST_DETAILS, new IdDt(), "param1", 2, List.of("param3", REQUEST_DETAILS)}; + assertThrows(InternalErrorException.class, () -> { + BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, inputParams); + }); + } + + + @Test + @Disabled + void buildMethodParams_withNullParameterAnnotations_shouldThrowInternalErrorException() throws Exception { +// when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); +// when(myMethod.getParameterAnnotations()).thenReturn(null); +// +// InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { +// BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); +// }); +// +// assertTrue(exception.getMessage().contains("Parameter annotations cannot be null")); } } From 23ae62b8c70a931d48ea1403a368de9e34be8543 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 17 Jan 2025 17:52:49 -0500 Subject: [PATCH 28/75] Small cleanup. --- .../fhir/rest/server/method/OperationEmbeddedParameter.java | 5 ++--- .../uhn/fhir/rest/server/method/OperationIdParamDetails.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 0e122e0bc4fb..32bf3f4cfcd7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -54,9 +55,7 @@ // LUKETODO: consider deleting whatever code may be unused public class OperationEmbeddedParameter implements IParameter { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationEmbeddedParameter.class); - - static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; + static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") private static final Class[] COMPOSITE_TYPES = new Class[0]; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index d3cd52000a7f..c1efe6d9371d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -16,7 +16,7 @@ class OperationIdParamDetails { // LUKETODO: private public final Integer myIdParamIndex; - public static OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); + public static final OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); public OperationIdParamDetails(@Nullable IdParam theIdParam, @Nullable Integer theIdParamIndex) { myIdParam = theIdParam; From 4fae95091ca0ae401e28ba32b35a406716ff2bb7 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sat, 18 Jan 2025 16:04:11 -0500 Subject: [PATCH 29/75] Add more testing for method reflection classes. --- ...seMethodBindingMethodParameterBuilder.java | 4 +- ...thodBindingMethodParameterBuilderTest.java | 273 +++---------- .../server/method/InnerClassesAndMethods.java | 241 ++++++++++++ .../rest/server/method/MethodUtilTest.java | 370 ++++++++++-------- 4 files changed, 522 insertions(+), 366 deletions(-) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 01df2e3a465a..f742ace8f731 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; +import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -20,10 +21,11 @@ import static java.util.function.Predicate.not; // LUKETODO: javadoc +// LUKETODO: should this be responsible for invoking the method as well? class BaseMethodBindingMethodParameterBuilder { private static final org.slf4j.Logger ourLog = - org.slf4j.LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); private BaseMethodBindingMethodParameterBuilder() {} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 3d390097a17c..42199ed55886 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -1,183 +1,34 @@ package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; -import java.util.Objects; -import java.util.StringJoiner; +import static ca.uhn.fhir.rest.server.method.BaseMethodBindingMethodParameterBuilder.buildMethodParams; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.*; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class BaseMethodBindingMethodParameterBuilderTest { - private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); - - private void superSimple() {} - - private void sampleMethodOperationParams( - @IdParam IIdType theIdType, - @OperationParam(name = "param1") String theParam1, - @OperationEmbeddedParam(name = "param2") Integer theParam2, - @OperationEmbeddedParam(name = "param3") List theParam3) { - // Sample method for testing - } - - private static class SampleParams { - @OperationEmbeddedParam(name = "param1") - private final String myParam1; - - @OperationEmbeddedParam(name = "param2") - private final Integer myParam2; - - @OperationEmbeddedParam(name = "param3") - private final List myParam3; - - public SampleParams(String myParam1, Integer myParam2, List myParam3) { - this.myParam1 = myParam1; - this.myParam2 = myParam2; - this.myParam3 = myParam3; - } - - public String getParam1() { - return myParam1; - } - - public Integer getParam2() { - return myParam2; - } - - public List getParam3() { - return myParam3; - } - - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - SampleParams that = (SampleParams) o; - return Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2) && Objects.equals(myParam3, that.myParam3); - } - - @Override - public int hashCode() { - return Objects.hash(myParam1, myParam2, myParam3); - } - - @Override - public String toString() { - return new StringJoiner(", ", SampleParams.class.getSimpleName() + "[", "]") - .add("myParam1='" + myParam1 + "'") - .add("myParam2=" + myParam2) - .add("myParam3=" + myParam3) - .toString(); - } - } - - private static class SampleParamsWithIdParam { - @IdParam - private final IIdType myId; - - @OperationEmbeddedParam(name = "param1") - private final String myParam1; - - @OperationEmbeddedParam(name = "param2") - private final Integer myParam2; - - @OperationEmbeddedParam(name = "param3") - private final List myParam3; - - public SampleParamsWithIdParam(IIdType myId, String myParam1, Integer myParam2, List myParam3) { - this.myId = myId; - this.myParam1 = myParam1; - this.myParam2 = myParam2; - this.myParam3 = myParam3; - } - - public IIdType getId() { - return myId; - } - - public String getParam1() { - return myParam1; - } - - public Integer getParam2() { - return myParam2; - } - - public List getParam3() { - return myParam3; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - SampleParamsWithIdParam that = (SampleParamsWithIdParam) o; - return Objects.equals(myId, that.myId) && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2) && Objects.equals(myParam3, that.myParam3); - } - - @Override - public int hashCode() { - return Objects.hash(myId, myParam1, myParam2, myParam3); - } - - @Override - public String toString() { - return new StringJoiner(", ", SampleParamsWithIdParam.class.getSimpleName() + "[", "]") - .add("myId=" + myId) - .add("myParam1='" + myParam1 + "'") - .add("myParam2=" + myParam2) - .add("myParam3=" + myParam3) - .toString(); - } - } - - private String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, SampleParams theParams) { - // return something arbitrary - return theRequestDetails.getId().getValue() + theParams.getParam1(); - } + // LUKETODO: assert Exception messages - private String sampleMethodEmbeddedTypeRequestDetailsLast(SampleParams theParams, RequestDetails theRequestDetails) { - // return something arbitrary - return theRequestDetails.getId().getValue() + theParams.getParam1(); - } + private static final org.slf4j.Logger ourLog = + LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilderTest.class); - private String sampleMethodEmbeddedTypeNoRequestDetails(SampleParams theParams) { - // return something arbitrary - return theParams.getParam1(); - } - - private String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, SampleParams theParams, RequestDetails theRequestDetails2) { - // return something arbitrary - return theRequestDetails1.getId().getValue() + theParams.getParam1(); - } - - private String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, SampleParamsWithIdParam theParams) { - // return something arbitrary - return theRequestDetails.getId().getValue() + theParams.getParam1(); - } - - private String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { - // return something arbitrary - return theRequestDetails.getId().getValue() + theParams.getParam1(); - } + private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); - private String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParams theParams) { - // return something arbitrary - return theParams.getParam1(); - } + private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); // LUKETODO: wrong params // LUKETODO: wrong param order @@ -185,113 +36,113 @@ private String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParams t // LUKETODO: RequestDetails in signature but not passed @Test - void happyPathOperationParamsEmptyParams() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = this.getClass().getDeclaredMethod("superSimple"); - final Object[] inputParams = new Object[] {}; + void happyPathOperationParamsEmptyParams() throws InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SUPER_SIMPLE); + final Object[] inputParams = new Object[]{}; - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); assertArrayEquals(inputParams, actualOutputParams); } @Test - void happyPathOperationParamsNonEmptyParams() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodOperationParams", IIdType.class, String.class, Integer.class, List.class); - final Object[] inputParams = new Object[] {new IdDt(), "param1", 2, List.of("param3")}; + void happyPathOperationParamsNonEmptyParams() throws InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class); + final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); assertArrayEquals(inputParams, actualOutputParams); } @Test - void happyPathOperationEmbeddedTypesNoRequestDetails() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeNoRequestDetails", SampleParams.class); - final Object[] inputParams = new Object[] {"param1", 2, List.of("param3")}; - final Object[] expectedOutputParams = new Object[] {new SampleParams("param1", 2, List.of("param3"))}; + void happyPathOperationEmbeddedTypesNoRequestDetails() throws InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Object[] inputParams = new Object[]{"param1", List.of("param2")}; + final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test - void happyPathOperationEmbeddedTypesRequestDetailsFirst() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsFirst", RequestDetails.class, SampleParams.class); - final Object[] inputParams = new Object[] {REQUEST_DETAILS, "param1", 2, List.of("param3")}; - final Object[] expectedOutputParams = new Object[] {REQUEST_DETAILS, new SampleParams("param1", 2, List.of("param3"))}; + void happyPathOperationEmbeddedTypesRequestDetailsFirst() throws InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; + final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test - void happyPathOperationEmbeddedTypesRequestDetailsLast() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsLast", SampleParams.class, RequestDetails.class); - final Object[] inputParams = new Object[] {"param1", 2, List.of("param3"), REQUEST_DETAILS}; - final Object[] expectedOutputParams = new Object[] {new SampleParams("param1", 2, List.of("param3")), REQUEST_DETAILS}; + void happyPathOperationEmbeddedTypesRequestDetailsLast() throws InvocationTargetException, IllegalAccessException, InstantiationException { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; + final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test - void happyPathOperationEmbeddedTypesWithIdType() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { - final IdDt id = new IdDt(); - final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType", RequestDetails.class, SampleParamsWithIdParam.class); - final Object[] inputParams = new Object[] {REQUEST_DETAILS, id, "param1", 2, List.of("param3")}; - final Object[] expectedOutputParams = new Object[] {REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", 2, List.of("param3")), }; - - final Object[] actualOutputParams = BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, inputParams); - - assertArrayEquals(expectedOutputParams, actualOutputParams); + @Disabled + // LUKETODO: Figure out what we're doing with FHIR structures in this test module before testing anything with IdTypes... + void happyPathOperationEmbeddedTypesWithIdType() throws InvocationTargetException, IllegalAccessException, InstantiationException { +// final IIdType id = new IIdType(); +// final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); +// final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2")}; +// final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2")),}; +// +// final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); +// +// assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, new Object[]{}); + buildMethodParams(null, new Object[]{}); }); } @Test void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws NoSuchMethodException { - final Method sampleMethod = this.getClass().getDeclaredMethod("superSimple"); + final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(sampleMethod, null); + buildMethodParams(sampleMethod, null); }); } @Test - void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException() throws NoSuchMethodException { - assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(null, null); - }); + void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException() { + assertThrows(InternalErrorException.class, () -> buildMethodParams(null, null)); } @Test - void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() throws NoSuchMethodException { - final Method method = this.getClass().getDeclaredMethod("sampleMethodEmbeddedTypeMultipleRequestDetails", RequestDetails.class, SampleParams.class, RequestDetails.class); - final Object[] inputParams = new Object[] {REQUEST_DETAILS, new IdDt(), "param1", 2, List.of("param3", REQUEST_DETAILS)}; + void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() { + final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, + RequestDetails.class, SampleParams.class, RequestDetails.class); + final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { - BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, inputParams); + buildMethodParams(method, inputParams); }); } - + // LUKETODO: decide what to do with this @Test @Disabled - void buildMethodParams_withNullParameterAnnotations_shouldThrowInternalErrorException() throws Exception { -// when(myMethod.getParameterTypes()).thenReturn(new Class[]{String.class}); -// when(myMethod.getParameterAnnotations()).thenReturn(null); -// -// InternalErrorException exception = assertThrows(InternalErrorException.class, () -> { -// BaseMethodBindingMethodParameterBuilder.buildMethodParams(myMethod, new Object[]{"param1"}); -// }); -// -// assertTrue(exception.getMessage().contains("Parameter annotations cannot be null")); + void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { + final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); + + final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; + + assertThrows(InternalErrorException.class, () -> { + buildMethodParams(method, inputParams); + }); } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java new file mode 100644 index 000000000000..c4a034c702bb --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -0,0 +1,241 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import static org.junit.jupiter.api.Assertions.fail; + +// Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes +class InnerClassesAndMethods { + + // LUKETODO: figure out how to test FHIR version specific resources and primitive types. + + static final String SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS = "sampleMethodEmbeddedTypeMultipleRequestDetails"; + static final String SUPER_SIMPLE = "superSimple"; + static final String INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION = "invalidMethodOperationParamsNoOperationInvalid"; + static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE = "sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType"; + static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST = "sampleMethodEmbeddedTypeRequestDetailsLast"; + static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST = "sampleMethodEmbeddedTypeRequestDetailsFirst"; + static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS = "sampleMethodEmbeddedTypeNoRequestDetails"; + static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; + static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; + + Method getDeclaredMethod(String theMethodName, Class... theParamClasses) { + try { + return this.getClass().getDeclaredMethod(theMethodName, theParamClasses); + } catch (Exception exceptional) { + fail(String.format("Could not find method: %s with params: %s", theMethodName, Arrays.toString(theParamClasses))); + } + return null; + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T)theObject; + } + + // Below are the methods and classed to test reflection code + + void superSimple() { + } + + void invalidMethodOperationParamsNoOperationInvalid( + @OperationParam(name = "param1") String theParam1) { + + } + + @Operation(name="sampleMethodOperationParams") + void sampleMethodOperationParams( + @IdParam IIdType theIdType, + @OperationParam(name = "param1") String theParam1, + @OperationParam(name = "param2") List theParam2) { + // Sample method for testing + } + + static class ParamsWithoutAnnotations { + private final String myParam1; + private final List myParam2; + + public ParamsWithoutAnnotations(String myParam1, List myParam2) { + this.myParam1 = myParam1; + this.myParam2 = myParam2; + } + + public String getParam1() { + return myParam1; + } + + public List getParam2() { + return myParam2; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + ParamsWithoutAnnotations that = (ParamsWithoutAnnotations) o; + return Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + + @Override + public String toString() { + return new StringJoiner(", ", ParamsWithoutAnnotations.class.getSimpleName() + "[", "]") + .add("myParam1='" + myParam1 + "'") + .add("myParam2=" + myParam2) + .toString(); + } + } + + // Ignore warnings that these classes can be records. Converting them to records will make the tests fail + static class SampleParams { + @OperationEmbeddedParam(name = "param1") + private final String myParam1; + + @OperationEmbeddedParam(name = "param2") + private final List myParam2; + + public SampleParams(String myParam1, List myParam2) { + this.myParam1 = myParam1; + this.myParam2 = myParam2; + } + + public String getParam1() { + return myParam1; + } + + public List getParam2() { + return myParam2; + } + + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SampleParams that = (SampleParams) o; + return Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + + @Override + public String toString() { + return new StringJoiner(", ", SampleParams.class.getSimpleName() + "[", "]") + .add("myParam1='" + myParam1 + "'") + .add("myParam2=" + myParam2) + .toString(); + } + } + + // Ignore warnings that these classes can be records. Converting them to records will make the tests fail + static class SampleParamsWithIdParam { + @IdParam + private final IIdType myId; + + @OperationEmbeddedParam(name = "param1") + private final String myParam1; + + @OperationEmbeddedParam(name = "param2") + private final List myParam2; + + public SampleParamsWithIdParam(IIdType myId, String myParam1, List myParam2) { + this.myId = myId; + this.myParam1 = myParam1; + this.myParam2 = myParam2; + } + + public IIdType getId() { + return myId; + } + + public String getParam1() { + return myParam1; + } + + public List getParam2() { + return myParam2; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SampleParamsWithIdParam that = (SampleParamsWithIdParam) o; + return Objects.equals(myId, that.myId) && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2); + } + + @Override + public int hashCode() { + return Objects.hash(myId, myParam1, myParam2); + } + + @Override + public String toString() { + return new StringJoiner(", ", SampleParamsWithIdParam.class.getSimpleName() + "[", "]") + .add("myId=" + myId) + .add("myParam1='" + myParam1 + "'") + .add("myParam2=" + myParam2) + .toString(); + } + } + + String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, SampleParams theParams) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + String sampleMethodEmbeddedTypeRequestDetailsLast(SampleParams theParams, RequestDetails theRequestDetails) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + @Operation(name="sampleMethodEmbeddedTypeNoRequestDetails") + String sampleMethodEmbeddedTypeNoRequestDetails(SampleParams theParams) { + // return something arbitrary + return theParams.getParam1(); + } + + String sampleMethodParamNoEmbeddedType(ParamsWithoutAnnotations theParams) { + // return something arbitrary + return theParams.getParam1(); + } + + String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, SampleParams theParams, RequestDetails theRequestDetails2) { + // return something arbitrary + return theRequestDetails1.getId().getValue() + theParams.getParam1(); + } + + String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, SampleParamsWithIdParam theParams) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { + // return something arbitrary + return theRequestDetails.getId().getValue() + theParams.getParam1(); + } + + String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParams theParams) { + // return something arbitrary + return theParams.getParam1(); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 49d2cfede3c1..cf6b55a90242 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -2,184 +2,246 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.annotation.OptionalParam; -import ca.uhn.fhir.rest.annotation.RequiredParam; -import org.hl7.fhir.instance.model.api.IBaseResource; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.IFhirVersion; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) class MethodUtilTest { - @Mock - private FhirContext myFhirContext; + private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); - @Mock - private Method method; + // Need FHIR structures in test pom to use this, which is only needed for a tiny number of test cases + @Mock + private FhirContext myFhirContext; - @Mock - private Object provider; + @Mock + private IFhirVersion myFhirVersion; - void sampleMethod(@RequiredParam(name = "param") String param) { - // Sample method for testing - } + private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); - @Test + @Mock + private Object myProvider; + + @BeforeEach + void beforeEach() { + lenient().when(myFhirVersion.getVersion()).thenReturn(FhirVersionEnum.R4); + lenient().when(myFhirContext.getVersion()).thenReturn(myFhirVersion); + } + + @Test + void simpleMethodNoParams() { + final List resourceParameters = getMethodAndExecute(SUPER_SIMPLE); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isEmpty(); + } + + @Test + void invalid_methodWithOperationParamsNoOperation() { + assertThatThrownBy( + () -> getMethodAndExecute(INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION, + String.class)) + .isInstanceOf(ConfigurationException.class); + } + + @Test + void sampleMethodOperationParams() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + + // LUKETODO: assert the actual OperationParameter values + } + + @Test + void sampleMethodEmbeddedParams() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + + // LUKETODO: assert the actual OperationEmbeddedParameter values + } + + @Test + @Disabled void getResourceParameters_withOptionalParam_shouldReturnSearchParameter() throws NoSuchMethodException { - Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); - when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new OptionalParam() { - @Override - public Class annotationType() { - return OptionalParam.class; - } - - @Override - public String[] chainBlacklist() { - return new String[0]; - } - - @Override - public String[] chainWhitelist() { - return new String[0]; - } - - @Override - public Class[] compositeTypes() { - return new Class[0]; - } - - @Override - public String name() { - return "param"; - } - - @Override - public Class[] targetTypes() { - return new Class[0]; - } - }}}); - when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); - - List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); - - assertEquals(1, parameters.size()); - assertInstanceOf(SearchParameter.class, parameters.get(0)); - SearchParameter searchParameter = (SearchParameter) parameters.get(0); - assertEquals("param", searchParameter.getName()); - assertFalse(searchParameter.isRequired()); +// final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); +// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new OptionalParam() { +// @Override +// public Class annotationType() { +// return OptionalParam.class; +// } +// +// @Override +// public String[] chainBlacklist() { +// return new String[0]; +// } +// +// @Override +// public String[] chainWhitelist() { +// return new String[0]; +// } +// +// @Override +// public Class[] compositeTypes() { +// return new Class[0]; +// } +// +// @Override +// public String name() { +// return "param"; +// } +// +// @Override +// public Class[] targetTypes() { +// return new Class[0]; +// } +// }}}); +// when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); +// +// List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); +// +// assertEquals(1, parameters.size()); +// assertInstanceOf(SearchParameter.class, parameters.get(0)); +// SearchParameter searchParameter = (SearchParameter) parameters.get(0); +// assertEquals("param", searchParameter.getName()); +// assertFalse(searchParameter.isRequired()); } @Test + @Disabled void getResourceParameters_withInvalidAnnotation_shouldThrowConfigurationException() throws NoSuchMethodException { - Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); - when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new Annotation() { - @Override - public Class annotationType() { - return Annotation.class; - } - }}}); - when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); - - ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { - MethodUtil.getResourceParameters(myFhirContext, method, provider); - }); - - assertTrue(exception.getMessage().contains("has no recognized FHIR interface parameter nextParameterAnnotations")); +// Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); +// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new Annotation() { +// @Override +// public Class annotationType() { +// return Annotation.class; +// } +// }}}); +// when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); +// +// ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { +// MethodUtil.getResourceParameters(myFhirContext, method, provider); +// }); +// +// assertTrue(exception.getMessage().contains("has no recognized FHIR interface parameter nextParameterAnnotations")); } @Test + @Disabled void getResourceParameters_withMultipleAnnotations_shouldReturnCorrectParameters() throws NoSuchMethodException { - Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); - when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{ - {new RequiredParam() { - @Override - public Class annotationType() { - return RequiredParam.class; - } - - @Override - public String[] chainBlacklist() { - return new String[0]; - } - - @Override - public String[] chainWhitelist() { - return new String[0]; - } - - @Override - public Class[] compositeTypes() { - return new Class[0]; - } - - @Override - public String name() { - return "param1"; - } - - @Override - public Class[] targetTypes() { - return new Class[0]; - } - }}, - {new OptionalParam() { - @Override - public Class annotationType() { - return OptionalParam.class; - } - - @Override - public String[] chainBlacklist() { - return new String[0]; - } - - @Override - public String[] chainWhitelist() { - return new String[0]; - } - - @Override - public Class[] compositeTypes() { - return new Class[0]; - } - - @Override - public String name() { - return "param2"; - } - - @Override - public Class[] targetTypes() { - return new Class[0]; - } - }} - }); - when(method.getParameterTypes()).thenReturn(new Class[]{String.class, String.class}); - - List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); - - assertEquals(2, parameters.size()); - assertTrue(parameters.get(0) instanceof SearchParameter); - assertTrue(parameters.get(1) instanceof SearchParameter); - SearchParameter searchParameter1 = (SearchParameter) parameters.get(0); - SearchParameter searchParameter2 = (SearchParameter) parameters.get(1); - assertEquals("param1", searchParameter1.getName()); - assertTrue(searchParameter1.isRequired()); - assertEquals("param2", searchParameter2.getName()); - assertFalse(searchParameter2.isRequired()); +// Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); +// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{ +// {new RequiredParam() { +// @Override +// public Class annotationType() { +// return RequiredParam.class; +// } +// +// @Override +// public String[] chainBlacklist() { +// return new String[0]; +// } +// +// @Override +// public String[] chainWhitelist() { +// return new String[0]; +// } +// +// @Override +// public Class[] compositeTypes() { +// return new Class[0]; +// } +// +// @Override +// public String name() { +// return "param1"; +// } +// +// @Override +// public Class[] targetTypes() { +// return new Class[0]; +// } +// }}, +// {new OptionalParam() { +// @Override +// public Class annotationType() { +// return OptionalParam.class; +// } +// +// @Override +// public String[] chainBlacklist() { +// return new String[0]; +// } +// +// @Override +// public String[] chainWhitelist() { +// return new String[0]; +// } +// +// @Override +// public Class[] compositeTypes() { +// return new Class[0]; +// } +// +// @Override +// public String name() { +// return "param2"; +// } +// +// @Override +// public Class[] targetTypes() { +// return new Class[0]; +// } +// }} +// }); +// when(method.getParameterTypes()).thenReturn(new Class[]{String.class, String.class}); +// +// List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); +// +// assertEquals(2, parameters.size()); +// assertTrue(parameters.get(0) instanceof SearchParameter); +// assertTrue(parameters.get(1) instanceof SearchParameter); +// SearchParameter searchParameter1 = (SearchParameter) parameters.get(0); +// SearchParameter searchParameter2 = (SearchParameter) parameters.get(1); +// assertEquals("param1", searchParameter1.getName()); +// assertTrue(searchParameter1.isRequired()); +// assertEquals("param2", searchParameter2.getName()); +// assertFalse(searchParameter2.isRequired()); } + + private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { + return MethodUtil.getResourceParameters( + myFhirContext, + myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), + myProvider); + } + + private List getResourceParameters(Method theMethod) { + return MethodUtil.getResourceParameters(myFhirContext, theMethod, myProvider); + } } From d221217578f0c1cd4e296699f313dc4522296726 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sat, 18 Jan 2025 17:55:29 -0500 Subject: [PATCH 30/75] TODOs. Slight refactoring. --- .../BaseMethodBindingMethodParameterBuilder.java | 11 ++++++++++- .../BaseMethodBindingMethodParameterBuilderTest.java | 1 + .../uhn/fhir/rest/server/method/MethodUtilTest.java | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index f742ace8f731..553441f36ca2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -29,7 +29,16 @@ class BaseMethodBindingMethodParameterBuilder { private BaseMethodBindingMethodParameterBuilder() {} - static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) + static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) { + try { + return tryBuildMethodParams(theMethod, theMethodParams); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException exception) { + throw new InternalErrorException(String.format("%s1234: Error building method params: %s", + Msg.code(234198928), exception.getMessage()), exception); + } + } + + static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { if (theMethod == null || theMethodParams == null) { diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 42199ed55886..3fcc6abc09a5 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +// LUKETODO: try to cover more InternalErrorException cases class BaseMethodBindingMethodParameterBuilderTest { // LUKETODO: assert Exception messages diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index cf6b55a90242..4edb03c5c5ea 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -25,6 +25,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.lenient; +// LUKETODO: ask #team-hdp about adding FHIR structures R4 to the test pom +// LUKETODO: test FHIR primitive types, like IntegerType, BooleanType, and IPrimitiveType +// LUKETODO: test IdParam/IIdType/etc +// LUKETODO: test with RequestDetails either at the beginning or the end +// LUKETODO: try to test for every case in embedded params where there's a throws @ExtendWith(MockitoExtension.class) class MethodUtilTest { From c2bfbf82b8d1b281db35fb3e654abc1a4f1c4468 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 19 Jan 2025 16:15:55 -0500 Subject: [PATCH 31/75] Import FHIR structures for R4 and enhance tests to leverage FHIR classes. --- hapi-fhir-server/pom.xml | 14 ++++++ ...thodBindingMethodParameterBuilderTest.java | 38 +++++++------- .../server/method/InnerClassesAndMethods.java | 40 +++++++++------ .../rest/server/method/MethodUtilTest.java | 50 +++++++++++-------- 4 files changed, 90 insertions(+), 52 deletions(-) diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 288146b4906e..d14efcc10dc1 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -112,6 +112,20 @@ org.apache.commons commons-collections4 + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${project.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${project.version} + sources + test + diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 3fcc6abc09a5..dac8b4df347d 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -5,7 +5,8 @@ import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; -import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; @@ -37,7 +38,7 @@ class BaseMethodBindingMethodParameterBuilderTest { // LUKETODO: RequestDetails in signature but not passed @Test - void happyPathOperationParamsEmptyParams() throws InvocationTargetException, IllegalAccessException, InstantiationException { + void happyPathOperationParamsEmptyParams() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; @@ -47,8 +48,8 @@ void happyPathOperationParamsEmptyParams() throws InvocationTargetException, Ill } @Test - void happyPathOperationParamsNonEmptyParams() throws InvocationTargetException, IllegalAccessException, InstantiationException { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class); + void happyPathOperationParamsNonEmptyParams() { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); @@ -57,7 +58,7 @@ void happyPathOperationParamsNonEmptyParams() throws InvocationTargetException, } @Test - void happyPathOperationEmbeddedTypesNoRequestDetails() throws InvocationTargetException, IllegalAccessException, InstantiationException { + void happyPathOperationEmbeddedTypesNoRequestDetails() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; @@ -68,7 +69,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() throws InvocationTargetEx } @Test - void happyPathOperationEmbeddedTypesRequestDetailsFirst() throws InvocationTargetException, IllegalAccessException, InstantiationException { + void happyPathOperationEmbeddedTypesRequestDetailsFirst() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; @@ -79,7 +80,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() throws InvocationTarge } @Test - void happyPathOperationEmbeddedTypesRequestDetailsLast() throws InvocationTargetException, IllegalAccessException, InstantiationException { + void happyPathOperationEmbeddedTypesRequestDetailsLast() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; @@ -91,16 +92,19 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() throws InvocationTarget @Test @Disabled - // LUKETODO: Figure out what we're doing with FHIR structures in this test module before testing anything with IdTypes... - void happyPathOperationEmbeddedTypesWithIdType() throws InvocationTargetException, IllegalAccessException, InstantiationException { -// final IIdType id = new IIdType(); -// final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); -// final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2")}; -// final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2")),}; -// -// final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); -// -// assertArrayEquals(expectedOutputParams, actualOutputParams); + void happyPathOperationEmbeddedTypesWithIdType() { + final IdType id = new IdType(); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); + final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; + final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; + + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + +// assertParamsEqual(expectedOutputParams, actualOutputParams); + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + private void assertParamsEqual(Object[] expectedOutputParams, Object[] actualOutputParams) { } @Test diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index c4a034c702bb..c16c25338a2d 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -5,7 +5,8 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; -import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IdType; import java.lang.reflect.Method; import java.util.Arrays; @@ -56,9 +57,10 @@ void invalidMethodOperationParamsNoOperationInvalid( @Operation(name="sampleMethodOperationParams") void sampleMethodOperationParams( - @IdParam IIdType theIdType, + @IdParam IdType theIdType, @OperationParam(name = "param1") String theParam1, - @OperationParam(name = "param2") List theParam2) { + @OperationParam(name = "param2") List theParam2, + @OperationParam(name="param3") BooleanType theParam3) { // Sample method for testing } @@ -148,7 +150,7 @@ public String toString() { // Ignore warnings that these classes can be records. Converting them to records will make the tests fail static class SampleParamsWithIdParam { @IdParam - private final IIdType myId; + private final IdType myId; @OperationEmbeddedParam(name = "param1") private final String myParam1; @@ -156,13 +158,17 @@ static class SampleParamsWithIdParam { @OperationEmbeddedParam(name = "param2") private final List myParam2; - public SampleParamsWithIdParam(IIdType myId, String myParam1, List myParam2) { - this.myId = myId; - this.myParam1 = myParam1; - this.myParam2 = myParam2; + @OperationEmbeddedParam(name = "param3") + private final BooleanType myParam3; + + public SampleParamsWithIdParam(IdType theId, String theParam1, List theParam2, BooleanType theParam3) { + myId = theId; + myParam1 = theParam1; + myParam2 = theParam2; + myParam3 = theParam3; } - public IIdType getId() { + public IdType getId() { return myId; } @@ -174,18 +180,23 @@ public List getParam2() { return myParam2; } + public BooleanType getMyParam3() { + return myParam3; + } + @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } + if (o == null || getClass() != o.getClass()) return false; SampleParamsWithIdParam that = (SampleParamsWithIdParam) o; - return Objects.equals(myId, that.myId) && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam2, that.myParam2); + return Objects.equals(myId, that.myId) && + Objects.equals(myParam1, that.myParam1) && + Objects.equals(myParam2, that.myParam2) && + Objects.equals(myParam3.booleanValue(), that.myParam3.booleanValue()); } @Override public int hashCode() { - return Objects.hash(myId, myParam1, myParam2); + return Objects.hash(myId, myParam1, myParam2, myParam3.booleanValue()); } @Override @@ -194,6 +205,7 @@ public String toString() { .add("myId=" + myId) .add("myParam1='" + myParam1 + "'") .add("myParam2=" + myParam2) + .add("myParam3=" + myParam3.booleanValue()) .toString(); } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 4edb03c5c5ea..4d682c266d08 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -2,11 +2,9 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.model.api.IFhirVersion; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; -import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.jupiter.api.BeforeEach; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,29 +28,17 @@ // LUKETODO: test IdParam/IIdType/etc // LUKETODO: test with RequestDetails either at the beginning or the end // LUKETODO: try to test for every case in embedded params where there's a throws -@ExtendWith(MockitoExtension.class) class MethodUtilTest { private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); - // Need FHIR structures in test pom to use this, which is only needed for a tiny number of test cases - @Mock - private FhirContext myFhirContext; - - @Mock - private IFhirVersion myFhirVersion; + private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); @Mock private Object myProvider; - @BeforeEach - void beforeEach() { - lenient().when(myFhirVersion.getVersion()).thenReturn(FhirVersionEnum.R4); - lenient().when(myFhirContext.getVersion()).thenReturn(myFhirVersion); - } - @Test void simpleMethodNoParams() { final List resourceParameters = getMethodAndExecute(SUPER_SIMPLE); @@ -71,11 +57,22 @@ void invalid_methodWithOperationParamsNoOperation() { @Test void sampleMethodOperationParams() { - final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class); + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); + + // LUKETODO: assert the actual OperationParameter values + } + + @Test + void sampleMethodOperationParamsWithFhirTypes() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); // LUKETODO: assert the actual OperationParameter values } @@ -91,6 +88,17 @@ void sampleMethodEmbeddedParams() { // LUKETODO: assert the actual OperationEmbeddedParameter values } + @Test + void sampleMethodEmbeddedParamsWithFhirTypes() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + + // LUKETODO: assert the actual OperationEmbeddedParameter values + } + @Test @Disabled void getResourceParameters_withOptionalParam_shouldReturnSearchParameter() throws NoSuchMethodException { @@ -241,12 +249,12 @@ void getResourceParameters_withMultipleAnnotations_shouldReturnCorrectParameters private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( - myFhirContext, + ourFhirContext, myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), myProvider); } private List getResourceParameters(Method theMethod) { - return MethodUtil.getResourceParameters(myFhirContext, theMethod, myProvider); + return MethodUtil.getResourceParameters(ourFhirContext, theMethod, myProvider); } } From d4defac699328d04354e9c7c57431ea79bf83d82 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 19 Jan 2025 17:49:21 -0500 Subject: [PATCH 32/75] Tweaks to existing tests and new test class. --- .../fhir/rest/server/method/MethodUtil.java | 2 +- .../server/method/InnerClassesAndMethods.java | 4 +- .../rest/server/method/MethodUtilTest.java | 8 +- .../method/OperationMethodBindingTest.java | 93 +++++++++++++++++++ 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 29d3853d6dea..83f59bd792d3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -210,7 +210,7 @@ public static List getResourceParameters( if (op == null) { throw new ConfigurationException(Msg.code(404) - + "@OperationParam detected on method that is not annotated with @Operation: " + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + methodToUse.toGenericString()); } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index c16c25338a2d..7245a211d7d0 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -28,6 +28,7 @@ class InnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST = "sampleMethodEmbeddedTypeRequestDetailsLast"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST = "sampleMethodEmbeddedTypeRequestDetailsFirst"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS = "sampleMethodEmbeddedTypeNoRequestDetails"; + static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE = "sampleMethodEmbeddedTypeNoRequestDetailsWithIdType"; static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; @@ -246,7 +247,8 @@ String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdPa return theRequestDetails.getId().getValue() + theParams.getParam1(); } - String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParams theParams) { + @Operation(name="sampleMethodEmbeddedTypeNoRequestDetailsWithIdType") + String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWithIdParam theParams) { // return something arbitrary return theParams.getParam1(); } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 4d682c266d08..1cdc8cdfc3e0 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Disabled; @@ -17,17 +18,18 @@ import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.lenient; // LUKETODO: ask #team-hdp about adding FHIR structures R4 to the test pom // LUKETODO: test FHIR primitive types, like IntegerType, BooleanType, and IPrimitiveType // LUKETODO: test IdParam/IIdType/etc // LUKETODO: test with RequestDetails either at the beginning or the end // LUKETODO: try to test for every case in embedded params where there's a throws +@ExtendWith(MockitoExtension.class) class MethodUtilTest { private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); @@ -90,11 +92,11 @@ void sampleMethodEmbeddedParams() { @Test void sampleMethodEmbeddedParamsWithFhirTypes() { - final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); // LUKETODO: assert the actual OperationEmbeddedParameter values } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java new file mode 100644 index 000000000000..2aeb3a7a3954 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -0,0 +1,93 @@ +package ca.uhn.fhir.rest.server.method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import org.hl7.fhir.instance.model.api.IBaseResource; +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; + +@ExtendWith(MockitoExtension.class) +class OperationMethodBindingTest { + + private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); + + private Method myMethod; + + @Mock + private Object provider; + + @Operation(name = "") + void invalidOperation() { + + } + + @Test + void constructor_withInvalidOperationName_shouldThrowConfigurationException() throws NoSuchMethodException { + myMethod = getClass().getDeclaredMethod("invalidOperation"); + + final Operation operation = myMethod.getAnnotation(Operation.class); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + }); + + assertTrue(exception.getMessage().contains("is annotated with @Operation but this annotation has no name defined")); + } + + @Test + void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() { + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setOperation("differentOperation"); + requestDetails.setRequestType(RequestTypeEnum.GET); + + OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, mock(Operation.class)); + + assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + + @Test + void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() { + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setOperation("$operationName"); + requestDetails.setRequestType(RequestTypeEnum.GET); + + Operation operation = mock(Operation.class); + when(operation.name()).thenReturn("$operationName"); + + OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + + assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + + @Test + void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() { + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.PUT); + + Operation operation = mock(Operation.class); + when(operation.name()).thenReturn("$operationName"); + + OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + + MethodNotAllowedException exception = assertThrows(MethodNotAllowedException.class, () -> { + binding.invokeServer(null, requestDetails, new Object[]{}); + }); + + assertTrue(exception.getMessage().contains("methodNotSupported")); + } +} From e52f6f9cc0829fc423a660b505a28b1c4b9a6d0f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 09:38:00 -0500 Subject: [PATCH 33/75] Move tests to hapi-fhir-structures-r4 to circumvent a circular dependency. Spotless. Fix tests in OperationMethodBindingTest. Spotless. --- hapi-fhir-server/pom.xml | 14 -- ...seMethodBindingMethodParameterBuilder.java | 54 ++--- .../fhir/rest/server/method/MethodUtil.java | 190 +++++++++--------- .../method/OperationMethodBindingTest.java | 47 +++-- .../r4/measure/CareGapsOperationProvider.java | 3 +- ...thodBindingMethodParameterBuilderTest.java | 24 +-- .../server/method/InnerClassesAndMethods.java | 1 + .../rest/server/method/MethodUtilTest.java | 2 + 8 files changed, 167 insertions(+), 168 deletions(-) rename {hapi-fhir-server => hapi-fhir-structures-r4}/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java (85%) rename {hapi-fhir-server => hapi-fhir-structures-r4}/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java (99%) rename {hapi-fhir-server => hapi-fhir-structures-r4}/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java (99%) diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index d14efcc10dc1..288146b4906e 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -112,20 +112,6 @@ org.apache.commons commons-collections4 - - - ca.uhn.hapi.fhir - hapi-fhir-structures-r4 - ${project.version} - test - - - ca.uhn.hapi.fhir - hapi-fhir-structures-r4 - ${project.version} - sources - test - diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 553441f36ca2..d1bef4a86409 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -33,8 +33,10 @@ static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) { try { return tryBuildMethodParams(theMethod, theMethodParams); } catch (InvocationTargetException | IllegalAccessException | InstantiationException exception) { - throw new InternalErrorException(String.format("%s1234: Error building method params: %s", - Msg.code(234198928), exception.getMessage()), exception); + throw new InternalErrorException( + String.format( + "%s1234: Error building method params: %s", Msg.code(234198928), exception.getMessage()), + exception); } } @@ -42,17 +44,20 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { if (theMethod == null || theMethodParams == null) { - throw new InternalErrorException(String.format("%s Either theMethod: %s or theMethodParams: %s is null", + throw new InternalErrorException(String.format( + "%s Either theMethod: %s or theMethodParams: %s is null", Msg.code(234198927), theMethod, Arrays.toString(theMethodParams))); } final Class[] methodParameterTypes = theMethod.getParameterTypes(); if (Arrays.stream(methodParameterTypes) - .filter(RequestDetails.class::isAssignableFrom).count() > 1) { + .filter(RequestDetails.class::isAssignableFrom) + .count() + > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", - Msg.code(924469635), theMethod.getName())); + "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", + Msg.code(924469635), theMethod.getName())); } final List> parameterTypesWithOperationEmbeddedParam = @@ -70,19 +75,19 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) } final long numRequestDetails = Arrays.stream(methodParameterTypes) - .filter(RequestDetails.class::isAssignableFrom) - .count(); + .filter(RequestDetails.class::isAssignableFrom) + .count(); if (numRequestDetails == 0 && methodParameterTypes.length > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than 1 params and no RequestDetails: %s", - Msg.code(924469634), theMethod.getName())); + "%s1234: Invalid operation with embedded parameters. Cannot have more than 1 params and no RequestDetails: %s", + Msg.code(924469634), theMethod.getName())); } if (numRequestDetails > 0 && methodParameterTypes.length > 2) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and a RequestDetails: %s", - Msg.code(924469634), theMethod.getName())); + "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and a RequestDetails: %s", + Msg.code(924469634), theMethod.getName())); } final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); @@ -122,9 +127,10 @@ private static Object buildOperationEmbeddedObject( validMethodParamTypes(methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { - throw new InternalErrorException(String.format("1234: mismatch between constructor args: %s and non-request details parameter args: %s", - Arrays.toString(constructor.getParameterTypes()), - Arrays.toString(methodParamsWithoutRequestDetails))); + throw new InternalErrorException(String.format( + "1234: mismatch between constructor args: %s and non-request details parameter args: %s", + Arrays.toString(constructor.getParameterTypes()), + Arrays.toString(methodParamsWithoutRequestDetails))); } return constructor.newInstance(methodParamsWithoutRequestDetails); @@ -164,16 +170,17 @@ private static Constructor validateAndGetConstructor(Class theParameterTyp // RequestDetails must be dealt with separately because there is no such concept in clinical-reasoning and the // operation params classes must be defined in that project @Nonnull - private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodParams, Object operationEmbeddedType) { + private static Object[] buildMethodParamsInCorrectPositions( + Object[] theMethodParams, Object operationEmbeddedType) { final List requestDetailsMultiple = Arrays.stream(theMethodParams) - .filter(RequestDetails.class::isInstance) - .map(RequestDetails.class::cast) - .collect(Collectors.toUnmodifiableList()); + .filter(RequestDetails.class::isInstance) + .map(RequestDetails.class::cast) + .collect(Collectors.toUnmodifiableList()); if (requestDetailsMultiple.size() > 1) { throw new InternalErrorException( - Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); + Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); } if (requestDetailsMultiple.isEmpty()) { @@ -183,8 +190,7 @@ private static Object[] buildMethodParamsInCorrectPositions(Object[] theMethodPa final RequestDetails requestDetails = requestDetailsMultiple.get(0); - final int indexOfRequestDetails = Arrays.asList(theMethodParams) - .indexOf(requestDetails); + final int indexOfRequestDetails = Arrays.asList(theMethodParams).indexOf(requestDetails); if (indexOfRequestDetails == 0) { // RequestDetails goes first @@ -224,8 +230,8 @@ private static void validateMethodParamType(Object methodParamAtIndex, Class "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex)); } - // Ex: Field is declared as an IIdType, but argument is an IdDt - } else if (! parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { + // Ex: Field is declared as an IIdType, but argument is an IdDt + } else if (!parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { throw new InternalErrorException(String.format( "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 83f59bd792d3..87453ec0a699 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -205,29 +205,31 @@ public static List getResourceParameters( // LUKETODO: this relies on new new embedded params explicitly leave OUT @OperationParam on parameters if (nextParameterAnnotations.length == 0) { // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); + final List> operationEmbeddedTypes = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + methodToUse, OperationEmbeddedParam.class); if (op == null) { throw new ConfigurationException(Msg.code(404) - + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " - + methodToUse.toGenericString()); + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + + methodToUse.toGenericString()); } // LUKETODO: try to combine into validateAndGet methods final List> operationEmbeddedTypesInner = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + methodToUse, OperationEmbeddedParam.class); if (operationEmbeddedTypesInner.size() > 1) { // LUKETODO: error throw new ConfigurationException(String.format( - "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), methodToUse.getName())); + "%sOnly one type with embedded params is supported for now for method: %s", + Msg.code(9999927), methodToUse.getName())); } // LUKETODO: else???? - if (!operationEmbeddedTypes.isEmpty()) { // ensure this is the opposite so we can flip between the two + if (!operationEmbeddedTypes + .isEmpty()) { // ensure this is the opposite so we can flip between the two // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE // METHOD!!!!!!!!!!!!!!!!!!!! ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); @@ -243,27 +245,27 @@ public static List getResourceParameters( if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, methodToUse.getName())); + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), fieldName, methodToUse.getName())); } if (fieldAnnotations.length > 1) { // LUKETODO: error throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, methodToUse.getName())); + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, methodToUse.getName())); } final Set annotationClassNames = Arrays.stream(fieldAnnotations) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(Collectors.toUnmodifiableSet()); + .map(Annotation::annotationType) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); ourLog.info( - "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", - fieldName, - fieldType.getName(), - annotationClassNames); + "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", + fieldName, + fieldType.getName(), + annotationClassNames); // This is the parameter on the field in question on the type with embedded params // class: ex @@ -274,11 +276,9 @@ public static List getResourceParameters( parameters.add(new NullParameter()); } else if (fieldAnnotation instanceof OperationEmbeddedParam) { final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter( - theContext, - fieldAnnotation, - op, - (OperationEmbeddedParam) fieldAnnotation); + getOperationEmbeddedParameter( + theContext, fieldAnnotation, op, (OperationEmbeddedParam) + fieldAnnotation); parameterType = fieldType; @@ -287,26 +287,25 @@ public static List getResourceParameters( Class> innerCollectionTypeInner = null; if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionTypeInner = - (Class>) parameterType; + innerCollectionTypeInner = (Class>) parameterType; parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); if (parameterTypeInner == null - && methodToUse.getDeclaringClass().isSynthetic()) { + && methodToUse.getDeclaringClass().isSynthetic()) { try { methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), parameterTypes); + .getDeclaringClass() + .getSuperclass() + .getMethod(methodToUse.getName(), parameterTypes); parameterTypeInner = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - methodToUse, paramIndex); + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter( + methodToUse, paramIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse - .getDeclaringClass() - .getSuperclass() + "'"); + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse + .getDeclaringClass() + .getSuperclass() + "'"); } } // LUKETODO: @@ -319,8 +318,7 @@ public static List getResourceParameters( // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterTypeInner)) { outerCollectionTypeInner = innerCollectionTypeInner; - innerCollectionTypeInner = - (Class>) parameterType; + innerCollectionTypeInner = (Class>) parameterType; // LUKETODO: come up with another method to do this for field params parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); // LUKETODO: @@ -332,39 +330,40 @@ public static List getResourceParameters( // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterTypeInner)) { throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse - .getDeclaringClass() - .getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + + methodToUse.getName() + + "' in type '" + + methodToUse + .getDeclaringClass() + .getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } // LUKETODO: do I need to worry about this: - /* + /* - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + methodToUse); + } + parameterType = newParameterType; + */ -// ourLog.info( -// "1234: about to initialize types: method: {}, outerCollectionType: {}, innerCollectionType: {}, parameterType: {}", -// methodToUse.getName(), -// outerCollectionType, -// innerCollectionType, -// parameterType); + // ourLog.info( + // "1234: about to initialize types: method: {}, outerCollectionType: {}, + // innerCollectionType: {}, parameterType: {}", + // methodToUse.getName(), + // outerCollectionType, + // innerCollectionType, + // parameterType); // LUKETODO: how to handle multiple parameters.add(param); ????? paramContexts.add(new ParamInitializationContext( - operationEmbeddedParameter, - parameterTypeInner, - outerCollectionTypeInner, - innerCollectionTypeInner)); + operationEmbeddedParameter, + parameterTypeInner, + outerCollectionTypeInner, + innerCollectionTypeInner)); // LUKETODO: nasty hack to skip the null check param = operationEmbeddedParameter; } else { @@ -489,7 +488,6 @@ public static List getResourceParameters( + methodToUse.toGenericString()); } - OperationParam operationParam = (OperationParam) nextAnnotation; String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); @@ -560,30 +558,30 @@ public Object outgoingClient(Object theObject) { if (nextAnnotation instanceof Validate.Profile) { if (!parameterType.equals(String.class)) { throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" - + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() - + " must be of type " + String.class.getName()); + + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + + " must be of type " + String.class.getName()); } String description = ParametersUtil.extractDescription(nextParameterAnnotations); List examples = ParametersUtil.extractExamples(nextParameterAnnotations); param = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_PROFILE, - 0, - 1, - description, - examples) - .setConverter(new IOperationParamConverter() { - @Override - public Object incomingServer(Object theObject) { - return theObject.toString(); - } + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_PROFILE, + 0, + 1, + description, + examples) + .setConverter(new IOperationParamConverter() { + @Override + public Object incomingServer(Object theObject) { + return theObject.toString(); + } - @Override - public Object outgoingClient(Object theObject) { - return ParametersUtil.createString(theContext, theObject.toString()); - } - }); + @Override + public Object outgoingClient(Object theObject) { + return ParametersUtil.createString(theContext, theObject.toString()); + } + }); } } } @@ -591,7 +589,9 @@ public Object outgoingClient(Object theObject) { // LUKETODO: do we need this or just add conditional logic? if (paramContexts.isEmpty() - || ! (param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add RequestDetails if it's last + || !(param + instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add + // RequestDetails if it's last paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } @@ -615,7 +615,8 @@ public Object outgoingClient(Object theObject) { } @Nonnull - private static OperationEmbeddedParameter getOperationEmbeddedParameter(FhirContext theContext, Annotation fieldAnnotation, Operation op, OperationEmbeddedParam operationParam) { + private static OperationEmbeddedParameter getOperationEmbeddedParameter( + FhirContext theContext, Annotation fieldAnnotation, Operation op, OperationEmbeddedParam operationParam) { final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; final String description = ParametersUtil.extractDescription(fieldAnnotationArray); final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); @@ -624,13 +625,12 @@ private static OperationEmbeddedParameter getOperationEmbeddedParameter(FhirCont // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning // repo return new OperationEmbeddedParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); } - } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 2aeb3a7a3954..72f683520849 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.rest.server.method; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; @@ -17,6 +16,7 @@ import java.lang.reflect.Method; +// LUKETODO: consider whether this test needs to live here or in r4 structures @ExtendWith(MockitoExtension.class) class OperationMethodBindingTest { @@ -32,13 +32,17 @@ void invalidOperation() { } + @Operation(name = "$simpleOperation") + void simpleOperation() { + + } + @Test void constructor_withInvalidOperationName_shouldThrowConfigurationException() throws NoSuchMethodException { myMethod = getClass().getDeclaredMethod("invalidOperation"); - final Operation operation = myMethod.getAnnotation(Operation.class); - ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + final ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { new OperationMethodBinding( IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); }); @@ -47,47 +51,52 @@ void constructor_withInvalidOperationName_shouldThrowConfigurationException() th } @Test - void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() { + void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() throws NoSuchMethodException { + myMethod = getClass().getDeclaredMethod("simpleOperation"); + final Operation operation = myMethod.getAnnotation(Operation.class); + final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setOperation("differentOperation"); requestDetails.setRequestType(RequestTypeEnum.GET); - OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, mock(Operation.class)); + final OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(requestDetails)); } @Test - void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() { + void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() throws NoSuchMethodException { + myMethod = getClass().getDeclaredMethod("simpleOperation"); + final Operation operation = myMethod.getAnnotation(Operation.class); + final SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setOperation("$operationName"); + requestDetails.setOperation("$simpleOperation"); requestDetails.setRequestType(RequestTypeEnum.GET); - Operation operation = mock(Operation.class); - when(operation.name()).thenReturn("$operationName"); - - OperationMethodBinding binding = new OperationMethodBinding( + final OperationMethodBinding binding = new OperationMethodBinding( IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } @Test - void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() { + void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() throws NoSuchMethodException { + myMethod = getClass().getDeclaredMethod("simpleOperation"); + final Operation operation = myMethod.getAnnotation(Operation.class); + final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.PUT); - Operation operation = mock(Operation.class); - when(operation.name()).thenReturn("$operationName"); - - OperationMethodBinding binding = new OperationMethodBinding( + final OperationMethodBinding binding = new OperationMethodBinding( IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); - MethodNotAllowedException exception = assertThrows(MethodNotAllowedException.class, () -> { + final MethodNotAllowedException exception = assertThrows(MethodNotAllowedException.class, () -> { binding.invokeServer(null, requestDetails, new Object[]{}); }); - assertTrue(exception.getMessage().contains("methodNotSupported")); + assertTrue(exception.getMessage().contains("HTTP Method PUT is not allowed for this operation.")); } + + // LUKETODO: add tests for new functionality } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index acd89070154b..0ed9646926e6 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.cr.r4.ICareGapsServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.BooleanType; @@ -97,7 +96,7 @@ public CareGapsOperationProvider( "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) public Parameters careGapsReport( - // LUKETODO: do NOT use @OperationParam if this is for embedded params and document this + // LUKETODO: do NOT use @OperationParam if this is for embedded params and document this RequestDetails theRequestDetails, CareGapsParams theParams) { return myR4CareGapsProcessorFactory diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java similarity index 85% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index dac8b4df347d..780dcd05f1ac 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; @@ -20,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +// LUKETODO: comment why this class lives in this module // LUKETODO: try to cover more InternalErrorException cases class BaseMethodBindingMethodParameterBuilderTest { @@ -39,7 +39,7 @@ class BaseMethodBindingMethodParameterBuilderTest { @Test void happyPathOperationParamsEmptyParams() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SUPER_SIMPLE); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); @@ -49,7 +49,7 @@ void happyPathOperationParamsEmptyParams() { @Test void happyPathOperationParamsNonEmptyParams() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); @@ -59,7 +59,7 @@ void happyPathOperationParamsNonEmptyParams() { @Test void happyPathOperationEmbeddedTypesNoRequestDetails() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; @@ -70,7 +70,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { @Test void happyPathOperationEmbeddedTypesRequestDetailsFirst() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; @@ -81,7 +81,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() { @Test void happyPathOperationEmbeddedTypesRequestDetailsLast() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; @@ -94,19 +94,15 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { @Disabled void happyPathOperationEmbeddedTypesWithIdType() { final IdType id = new IdType(); - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); -// assertParamsEqual(expectedOutputParams, actualOutputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } - private void assertParamsEqual(Object[] expectedOutputParams, Object[] actualOutputParams) { - } - @Test void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { assertThrows(InternalErrorException.class, () -> { @@ -116,7 +112,7 @@ void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { @Test void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws NoSuchMethodException { - final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(SUPER_SIMPLE); + final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { buildMethodParams(sampleMethod, null); @@ -130,7 +126,7 @@ void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException @Test void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, + final Method method = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, RequestDetails.class, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { @@ -142,7 +138,7 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( @Test @Disabled void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); + final Method method = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java similarity index 99% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 7245a211d7d0..478774d05a09 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.fail; +// LUKETODO: comment why this class lives in this module // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes class InnerClassesAndMethods { diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java similarity index 99% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 1cdc8cdfc3e0..3062dae9ab83 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -29,6 +29,8 @@ // LUKETODO: test IdParam/IIdType/etc // LUKETODO: test with RequestDetails either at the beginning or the end // LUKETODO: try to test for every case in embedded params where there's a throws + +// LUKETODO: comment why this class lives in this module @ExtendWith(MockitoExtension.class) class MethodUtilTest { From 483792ef9f7136e05ee82f089829f2984e9384a2 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 10:50:22 -0500 Subject: [PATCH 34/75] Fix animal sniffer error. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 2 +- .../server/method/InnerClassesAndMethods.java | 22 ++++++- .../method/OperationMethodBindingTest.java | 59 +++++++++++-------- 3 files changed, 57 insertions(+), 26 deletions(-) rename {hapi-fhir-server => hapi-fhir-structures-r4}/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java (69%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 9cd400600368..5be7eb78ed79 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -318,7 +318,7 @@ public static List> getMethodParamsWithClassesWithFieldsWithAnnotation( Method theMethod, Class theAnnotationClass) { return Arrays.stream(theMethod.getParameterTypes()) .filter(paramType -> hasFieldsWithAnnotation(paramType, theAnnotationClass)) - .collect(Collectors.toUnmodifiableList()); + .collect(Collectors.toList()); } private static boolean hasFieldsWithAnnotation(Class paramType, Class theAnnotationClass) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 478774d05a09..725c835f717e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -7,6 +7,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; import java.lang.reflect.Method; import java.util.Arrays; @@ -52,18 +54,34 @@ private static T unsafeCast(Object theObject) { void superSimple() { } + @Operation(name = "") + void invalidOperation() { + + } + + @Operation(name = "$simpleOperation") + void simpleOperation() { + + } + + @Operation(name = "$withEmbeddedParams") + void withEmbeddedParams() { + + } + void invalidMethodOperationParamsNoOperationInvalid( @OperationParam(name = "param1") String theParam1) { } - @Operation(name="sampleMethodOperationParams") - void sampleMethodOperationParams( + @Operation(name="sampleMethodOperationParams", type = Measure.class) + public MeasureReport sampleMethodOperationParams( @IdParam IdType theIdType, @OperationParam(name = "param1") String theParam1, @OperationParam(name = "param2") List theParam2, @OperationParam(name="param3") BooleanType theParam3) { // Sample method for testing + return new MeasureReport(null, null, null, null); } static class ParamsWithoutAnnotations { diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java similarity index 69% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 72f683520849..fcb092afb5cf 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -9,12 +9,16 @@ import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.ResourceType; 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; // LUKETODO: consider whether this test needs to live here or in r4 structures @ExtendWith(MockitoExtension.class) @@ -22,29 +26,21 @@ class OperationMethodBindingTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); + private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); + private Method myMethod; + private Operation myOperation; @Mock private Object provider; - @Operation(name = "") - void invalidOperation() { - - } - - @Operation(name = "$simpleOperation") - void simpleOperation() { - - } - @Test - void constructor_withInvalidOperationName_shouldThrowConfigurationException() throws NoSuchMethodException { - myMethod = getClass().getDeclaredMethod("invalidOperation"); - final Operation operation = myMethod.getAnnotation(Operation.class); + void constructor_withInvalidOperationName_shouldThrowConfigurationException() { + init("invalidOperation"); final ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); }); assertTrue(exception.getMessage().contains("is annotated with @Operation but this annotation has no name defined")); @@ -52,44 +48,41 @@ void constructor_withInvalidOperationName_shouldThrowConfigurationException() th @Test void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() throws NoSuchMethodException { - myMethod = getClass().getDeclaredMethod("simpleOperation"); - final Operation operation = myMethod.getAnnotation(Operation.class); + init("simpleOperation"); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setOperation("differentOperation"); requestDetails.setRequestType(RequestTypeEnum.GET); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(requestDetails)); } @Test void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() throws NoSuchMethodException { - myMethod = getClass().getDeclaredMethod("simpleOperation"); - final Operation operation = myMethod.getAnnotation(Operation.class); + init("simpleOperation"); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setOperation("$simpleOperation"); requestDetails.setRequestType(RequestTypeEnum.GET); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } @Test void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() throws NoSuchMethodException { - myMethod = getClass().getDeclaredMethod("simpleOperation"); - final Operation operation = myMethod.getAnnotation(Operation.class); + init("simpleOperation"); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.PUT); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, operation); + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); final MethodNotAllowedException exception = assertThrows(MethodNotAllowedException.class, () -> { binding.invokeServer(null, requestDetails, new Object[]{}); @@ -98,5 +91,25 @@ void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedExceptio assertTrue(exception.getMessage().contains("HTTP Method PUT is not allowed for this operation.")); } + @Test + void simpleMethodOperationParams() throws NoSuchMethodException { + init("sampleMethodOperationParams", IdType.class, String.class, List.class, BooleanType.class); + + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.PUT); + requestDetails.setOperation("$sampleMethodOperationParams"); + requestDetails.setResourceName(ResourceType.MeasureReport.name()); + + final OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + + assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + + private void init(String theMethodName, Class... theParamClasses) { + myMethod = myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses); + myOperation = myMethod.getAnnotation(Operation.class); + } + // LUKETODO: add tests for new functionality } From 32bee05a9dd70c9f5f2fc05747f6a9815f1e72d1 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 13:25:28 -0500 Subject: [PATCH 35/75] Fix caregaps error by reusing OperationParameter.REQUEST_CONTENTS_USERDATA_KEY. Add more testing capabilities to hapi-fhir-server reflection testing. --- .../method/OperationEmbeddedParameter.java | 6 ++-- .../server/method/OperationMethodBinding.java | 1 + .../server/method/InnerClassesAndMethods.java | 9 +++--- .../method/OperationMethodBindingTest.java | 29 +++++++++++++++---- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 32bf3f4cfcd7..f8ab632b9d63 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -49,13 +48,16 @@ import java.util.function.Consumer; import java.util.*; +import static ca.uhn.fhir.rest.server.method.OperationParameter.REQUEST_CONTENTS_USERDATA_KEY; import static org.apache.commons.lang3.StringUtils.isNotBlank; // LUKETODO: use this for Embedded object params // LUKETODO: consider deleting whatever code may be unused public class OperationEmbeddedParameter implements IParameter { - static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; + // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? + // LUKETODO: if so, add conditional logic everywhere to use it +// static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") private static final Class[] COMPOSITE_TYPES = new Class[0]; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 9998aae0fec5..64362ff1ec08 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -482,6 +482,7 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( Method theMethod, List> theTypesWithEmbeddedParams, FhirContext theContext) { for (Class typeWithEmbeddedParams : theTypesWithEmbeddedParams) { + // LUKETODO: skip anything else? if (ParametersUtil.isOneOfEligibleTypes( typeWithEmbeddedParams, RequestDetails.class, SystemRequestDetails.class)) { // skip diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 725c835f717e..7cfe9ddbbe70 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; @@ -76,7 +77,7 @@ void invalidMethodOperationParamsNoOperationInvalid( @Operation(name="sampleMethodOperationParams", type = Measure.class) public MeasureReport sampleMethodOperationParams( - @IdParam IdType theIdType, + @IdParam IIdType theIdType, @OperationParam(name = "param1") String theParam1, @OperationParam(name = "param2") List theParam2, @OperationParam(name="param3") BooleanType theParam3) { @@ -266,9 +267,9 @@ String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdPa return theRequestDetails.getId().getValue() + theParams.getParam1(); } - @Operation(name="sampleMethodEmbeddedTypeNoRequestDetailsWithIdType") - String sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWithIdParam theParams) { + @Operation(name="sampleMethodEmbeddedTypeNoRequestDetailsWithIdType", type = Measure.class) + MeasureReport sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWithIdParam theParams) { // return something arbitrary - return theParams.getParam1(); + return new MeasureReport(null, null, null, null); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index fcb092afb5cf..9c2a0f34f6c0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -8,7 +8,9 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.ResourceType; @@ -92,13 +94,30 @@ void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedExceptio } @Test - void simpleMethodOperationParams() throws NoSuchMethodException { - init("sampleMethodOperationParams", IdType.class, String.class, List.class, BooleanType.class); + void simpleMethodOperationParams() { + init(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setRequestType(RequestTypeEnum.PUT); + requestDetails.setRequestType(RequestTypeEnum.GET); requestDetails.setOperation("$sampleMethodOperationParams"); - requestDetails.setResourceName(ResourceType.MeasureReport.name()); + requestDetails.setResourceName(ResourceType.Measure.name()); + requestDetails.setId(new IdType(ResourceType.Measure.name(), "Measure/123")); + + final OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + + assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + + @Test + void simpleMethodEmbeddedParams() { + init(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); + + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setOperation("$sampleMethodEmbeddedTypeNoRequestDetailsWithIdType"); + requestDetails.setResourceName(ResourceType.Measure.name()); + requestDetails.setId(new IdType(ResourceType.Measure.name(), "Measure/123")); final OperationMethodBinding binding = new OperationMethodBinding( IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); @@ -110,6 +129,4 @@ private void init(String theMethodName, Class... theParamClasses) { myMethod = myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses); myOperation = myMethod.getAnnotation(Operation.class); } - - // LUKETODO: add tests for new functionality } From abca3214f22b919033fe4c0bde3928796b50c123 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 14:08:23 -0500 Subject: [PATCH 36/75] Fix bug with wrong boolean variable assignment. --- .../ResourceProviderDstu2ValueSetTest.java | 2 -- .../server/method/OperationMethodBinding.java | 2 +- .../server/method/InnerClassesAndMethods.java | 14 ++++++++ .../method/OperationMethodBindingTest.java | 32 +++++++++++++++---- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java index 4b4971ba0773..72d1d6632e98 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java @@ -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 { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 64362ff1ec08..2341d8724c05 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -192,7 +192,7 @@ protected OperationMethodBinding( } else { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; myCanOperateAtInstanceLevel = true; - myCanOperateAtServerLevel = myOperationIdParamDetails.setOrReturnPreviousValue(myCanOperateAtServerLevel); + myCanOperateAtTypeLevel = myOperationIdParamDetails.setOrReturnPreviousValue(myCanOperateAtTypeLevel); } myReturnParams = new ArrayList<>(); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 7cfe9ddbbe70..94eb1bd21d1c 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -5,11 +5,14 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Patient; import java.lang.reflect.Method; import java.util.Arrays; @@ -27,6 +30,7 @@ class InnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS = "sampleMethodEmbeddedTypeMultipleRequestDetails"; static final String SUPER_SIMPLE = "superSimple"; + static final String SIMPLE_OPERATION = "simpleOperation"; static final String INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION = "invalidMethodOperationParamsNoOperationInvalid"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE = "sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST = "sampleMethodEmbeddedTypeRequestDetailsLast"; @@ -35,6 +39,7 @@ class InnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE = "sampleMethodEmbeddedTypeNoRequestDetailsWithIdType"; static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; + static final String EXPAND = "expand"; Method getDeclaredMethod(String theMethodName, Class... theParamClasses) { try { @@ -272,4 +277,13 @@ MeasureReport sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWit // return something arbitrary return new MeasureReport(null, null, null, null); } + + @Operation(name = "$expand", idempotent = true, typeName = "ValueSet") + public IBaseResource expand( + HttpServletRequest theServletRequest, + @IdParam(optional = true) IIdType theId, + @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet, + RequestDetails theRequestDetails) { + return new Patient(); + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 9c2a0f34f6c0..d1508df1b636 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -1,14 +1,19 @@ package ca.uhn.fhir.rest.server.method; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static org.junit.jupiter.api.Assertions.*; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; +import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; @@ -49,7 +54,7 @@ void constructor_withInvalidOperationName_shouldThrowConfigurationException() { } @Test - void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() throws NoSuchMethodException { + void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone() { init("simpleOperation"); final SystemRequestDetails requestDetails = new SystemRequestDetails(); @@ -63,7 +68,7 @@ void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone } @Test - void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() throws NoSuchMethodException { + void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact() { init("simpleOperation"); final SystemRequestDetails requestDetails = new SystemRequestDetails(); @@ -77,8 +82,8 @@ void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact( } @Test - void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() throws NoSuchMethodException { - init("simpleOperation"); + void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() { + init(InnerClassesAndMethods.SIMPLE_OPERATION); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.PUT); @@ -95,7 +100,7 @@ void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedExceptio @Test void simpleMethodOperationParams() { - init(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); + init(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.GET); @@ -111,7 +116,7 @@ void simpleMethodOperationParams() { @Test void simpleMethodEmbeddedParams() { - init(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); + init(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.GET); @@ -125,6 +130,21 @@ void simpleMethodEmbeddedParams() { assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } + @Test + void expandEnsureMethodEnsureCanOperateAtTypeLevel() { + init(EXPAND, HttpServletRequest.class, IIdType.class, IBaseResource.class, RequestDetails.class); + + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.POST); + requestDetails.setOperation("$expand"); + requestDetails.setResourceName(ResourceType.ValueSet.name()); + + final OperationMethodBinding binding = new OperationMethodBinding( + IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + + assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + private void init(String theMethodName, Class... theParamClasses) { myMethod = myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses); myOperation = myMethod.getAnnotation(Operation.class); From 287ca14265ad32a16eef240c2eb6205922dfe90d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 14:11:58 -0500 Subject: [PATCH 37/75] Spotless. --- .../uhn/fhir/rest/server/method/OperationEmbeddedParameter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index f8ab632b9d63..75b9743b713c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -57,7 +57,7 @@ public class OperationEmbeddedParameter implements IParameter { // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? // LUKETODO: if so, add conditional logic everywhere to use it -// static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; + // static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") private static final Class[] COMPOSITE_TYPES = new Class[0]; From 2aee1798cd4baab5e49efaacd55be18fad65e53a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 16:34:11 -0500 Subject: [PATCH 38/75] Clean up a lot of TODOs. Test tweaks. First attempt at refactoring complex new code in MethodUtil. Fix bug with OperationMethodBinding and non-IIDTypes. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 19 --- .../rest/server/method/BaseMethodBinding.java | 15 --- .../fhir/rest/server/method/MethodUtil.java | 116 ++++++++++++++---- .../MethodUtilMutableLoopStateHolder.java | 46 +++++++ ...MethodUtilParamInitializationContext.java} | 4 +- .../method/OperationIdParamDetails.java | 3 +- .../server/method/OperationMethodBinding.java | 30 +++-- .../r4/measure/MeasureOperationsProvider.java | 1 - ...thodBindingMethodParameterBuilderTest.java | 3 +- .../server/method/InnerClassesAndMethods.java | 46 ++++++- .../rest/server/method/MethodUtilTest.java | 34 ++++- .../method/OperationMethodBindingTest.java | 44 ++++--- 12 files changed, 258 insertions(+), 103 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java rename hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/{ParamInitializationContext.java => MethodUtilParamInitializationContext.java} (91%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 5be7eb78ed79..83d3e00d7baa 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -295,25 +295,6 @@ public static boolean typeExists(String theName) { } } - // LUKETODO: see if you can get rid of this: - public static boolean hasAnyMethodParamsWithClassesOfAnnotation( - Method theMethod, Class theAnnotationClass) { - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - return Arrays.stream(theMethod.getParameterTypes()) - .anyMatch(paramType -> paramType.isAnnotationPresent(theAnnotationClass)); - } - - // LUKETODO: use this whenever possible - public static boolean hasAnyMethodParametersContainingFieldsWithAnnotation( - Method theMethod, Class theAnnotationClass) { - return Arrays.stream(theMethod.getParameterTypes()) - .map(Class::getFields) - .map(Arrays::asList) - .flatMap(Collection::stream) - .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); - } - - // public static List> getMethodParamsWithClassesWithFieldsWithAnnotation( Method theMethod, Class theAnnotationClass) { return Arrays.stream(theMethod.getParameterTypes()) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 67c3f67265cf..3a86e6444704 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -122,19 +122,6 @@ protected Object[] createMethodParams(RequestDetails theRequest) { return params; } - // LUKETODO: JP deleted this for some reason: why? - protected Object[] createParametersForServerRequest(RequestDetails theRequest) { - Object[] params = new Object[getParameters().size()]; - for (int i = 0; i < getParameters().size(); i++) { - IParameter param = getParameters().get(i); - if (param == null) { - continue; - } - params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); - } - return params; - } - /** * Subclasses may override to declare that they apply to all resource types */ @@ -262,8 +249,6 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // Actually invoke the method try { - // LUKETODO: check to see if we have a single - // class, bind to the fields, then invoke. final Method method = getMethod(); return method.invoke( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 87453ec0a699..43e7c0662cfb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -116,7 +116,7 @@ public static List getResourceParameters( for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { // LUKETODO: wrapper object for all of these IParameter param = null; - final List paramContexts = new ArrayList<>(); + final List paramContexts = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -339,31 +339,18 @@ public static List getResourceParameters( + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } - // LUKETODO: do I need to worry about this: - /* + final MethodUtilMutableLoopStateHolder stateHolder = doStuff(methodToUse, parameterTypes, parameterType, paramIndex, field); - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ - - // ourLog.info( - // "1234: about to initialize types: method: {}, outerCollectionType: {}, - // innerCollectionType: {}, parameterType: {}", - // methodToUse.getName(), - // outerCollectionType, - // innerCollectionType, - // parameterType); + parameterType = stateHolder.getParameterType(); + methodToUse = stateHolder.getMethodToUse(); // LUKETODO: how to handle multiple parameters.add(param); ????? - paramContexts.add(new ParamInitializationContext( + paramContexts.add(new MethodUtilParamInitializationContext( operationEmbeddedParameter, - parameterTypeInner, - outerCollectionTypeInner, - innerCollectionTypeInner)); + stateHolder.getParameterType(), + stateHolder.getOuterCollectionType(), + stateHolder.getInnerCollectionType())); + // LUKETODO: nasty hack to skip the null check param = operationEmbeddedParameter; } else { @@ -593,7 +580,7 @@ public Object outgoingClient(Object theObject) { instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add // RequestDetails if it's last paramContexts.add( - new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); + new MethodUtilParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } if (param == null) { @@ -604,7 +591,7 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } - for (ParamInitializationContext paramContext : paramContexts) { + for (MethodUtilParamInitializationContext paramContext : paramContexts) { paramContext.initialize(methodToUse); parameters.add(paramContext.getParam()); } @@ -614,6 +601,87 @@ public Object outgoingClient(Object theObject) { return parameters; } + private static MethodUtilMutableLoopStateHolder doStuff(Method theMethod, Class[] theParameterTypes, Class theParameterType, int paramIndex, Field theField) { + Class parameterType = theParameterType; + Method methodToUse = theMethod; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = (Class>) parameterType; + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + if (parameterType == null + && methodToUse.getDeclaringClass().isSynthetic()) { + try { + methodToUse = methodToUse + .getDeclaringClass() + .getSuperclass() + .getMethod(methodToUse.getName(), theParameterTypes); + parameterType = + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter( + methodToUse, paramIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse + .getDeclaringClass() + .getSuperclass() + "'"); + } + } + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: now we're processing the generic parameter, so capture the inner and + // outer + // types + // Collection + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: as a guard: if this is still a Collection, then throw because + // something went + // wrong + // LUKETODO: could be null? + if (Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException( + Msg.code(401) + "Argument #" + paramIndex + " of Method '" + + methodToUse.getName() + + "' in type '" + + methodToUse + .getDeclaringClass() + .getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + // LUKETODO: do I need to worry about this: + /* + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + methodToUse); + } + parameterType = newParameterType; + */ + + // ourLog.info( + // "1234: about to initialize types: method: {}, outerCollectionType: {}, + // innerCollectionType: {}, parameterType: {}", + // methodToUse.getName(), + // outerCollectionType, + // innerCollectionType, + // parameterType); + + return new MethodUtilMutableLoopStateHolder(parameterType, methodToUse, outerCollectionType, innerCollectionType); + } + @Nonnull private static OperationEmbeddedParameter getOperationEmbeddedParameter( FhirContext theContext, Annotation fieldAnnotation, Operation op, OperationEmbeddedParam operationParam) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java new file mode 100644 index 000000000000..9fc82a4bc9a2 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.rest.server.method; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.StringJoiner; + +// LUKETODO: javadoc +class MethodUtilMutableLoopStateHolder { + private final Class myParameterType; + private final Method myMethodToUse; + private final Class> myOuterCollectionType; + private final Class> myInnerCollectionType; + + public MethodUtilMutableLoopStateHolder(Class theParameterType, Method theMethodToUse, Class> theOuterCollectionType, Class> theInnerCollectionType) { + myParameterType = theParameterType; + myMethodToUse = theMethodToUse; + myOuterCollectionType = theOuterCollectionType; + myInnerCollectionType = theInnerCollectionType; + } + + public Class getParameterType() { + return myParameterType; + } + + public Method getMethodToUse() { + return myMethodToUse; + } + + public Class> getOuterCollectionType() { + return myOuterCollectionType; + } + + public Class> getInnerCollectionType() { + return myInnerCollectionType; + } + + @Override + public String toString() { + return new StringJoiner(", ", MethodUtilMutableLoopStateHolder.class.getSimpleName() + "[", "]") + .add("myParameterType=" + myParameterType) + .add("myMethodToUse=" + myMethodToUse) + .add("myOuterCollectionType=" + myOuterCollectionType) + .add("myInnerCollectionType=" + myInnerCollectionType) + .toString(); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java similarity index 91% rename from hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java index 60bec09edc13..3410de576462 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java @@ -4,13 +4,13 @@ import java.util.Collection; // LUKETODO: javadoc -class ParamInitializationContext { +class MethodUtilParamInitializationContext { private final IParameter myParam; private final Class myParameterType; private final Class> myOuterCollectionType; private final Class> myInnerCollectionType; - ParamInitializationContext( + MethodUtilParamInitializationContext( IParameter theParam, Class theParameterType, Class> theOuterCollectionType, diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index c1efe6d9371d..59cd3ff68972 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -13,8 +13,7 @@ class OperationIdParamDetails { private final IdParam myIdParam; @Nullable - // LUKETODO: private - public final Integer myIdParamIndex; + private final Integer myIdParamIndex; public static final OperationIdParamDetails EMPTY = new OperationIdParamDetails(null, null); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 2341d8724c05..80f0441a4da0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; @@ -54,8 +55,10 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -458,26 +461,30 @@ private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirCon @Nonnull private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theMethod, FhirContext theContext) { - final Integer paramAnnotationIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); - + final Integer paramAnnotationIndex = ParameterUtil.findParamAnnotationIndex(theMethod, IdParam.class); if (paramAnnotationIndex != null) { + final Optional optIdParam = findIdParam(theMethod, paramAnnotationIndex); final Class paramType = theMethod.getParameterTypes()[paramAnnotationIndex]; if (IIdType.class.equals(paramType)) { - final Annotation[] parameterAnnotation = theMethod.getParameterAnnotations()[paramAnnotationIndex]; - - for (Annotation nextParameterAnnotation : parameterAnnotation) { - if (nextParameterAnnotation instanceof IdParam) { - return new OperationIdParamDetails((IdParam) nextParameterAnnotation, paramAnnotationIndex); - } - } + return new OperationIdParamDetails(optIdParam.orElse(null), paramAnnotationIndex); } - return new OperationIdParamDetails(null, paramAnnotationIndex); + ParameterUtil.validateIdType(theMethod, theContext, paramType); + + return new OperationIdParamDetails(optIdParam.orElse(null), paramAnnotationIndex); } return OperationIdParamDetails.EMPTY; } + private Optional findIdParam(Method theMethod, int theParamIndex) { + // LUKETODO: do we need to validate there's only one? + return Arrays.stream(theMethod.getParameterAnnotations()[theParamIndex]) + .filter(IdParam.class::isInstance) + .map(IdParam.class::cast) + .findFirst(); + } + @Nonnull private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( Method theMethod, List> theTypesWithEmbeddedParams, FhirContext theContext) { @@ -497,15 +504,12 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( final Annotation[] fieldAnnotations = field.getAnnotations(); if (fieldAnnotations.length < 1) { - // LUKETODO: which Exception should we throw here? throw new ConfigurationException(String.format( "%sNo annotations for field: %s for method: %s", Msg.code(126362643), fieldName, theMethod.getName())); } if (fieldAnnotations.length > 1) { - // LUKETODO: error - // LUKETODO: which Exception should we throw here? throw new ConfigurationException(String.format( "%sMore than one annotation for field: %s for method: %s", Msg.code(195614846), fieldName, theMethod.getName())); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 17ce9e9b38f0..0841fd56f10d 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -72,7 +72,6 @@ public MeasureOperationsProvider( @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE, idempotent = true, type = Measure.class) public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) throws InternalErrorException, FHIRException { - // LUKETODO: Parameters within Parameters return myR4MeasureServiceFactory .create(theRequestDetails) .evaluate( diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 780dcd05f1ac..c38ad072829a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Disabled; @@ -49,7 +50,7 @@ void happyPathOperationParamsEmptyParams() { @Test void happyPathOperationParamsNonEmptyParams() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 94eb1bd21d1c..8fffceaa58d0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -5,6 +5,8 @@ import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.IResourceProvider; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -12,12 +14,15 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.StringJoiner; import static org.junit.jupiter.api.Assertions.fail; @@ -26,8 +31,6 @@ // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes class InnerClassesAndMethods { - // LUKETODO: figure out how to test FHIR version specific resources and primitive types. - static final String SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS = "sampleMethodEmbeddedTypeMultipleRequestDetails"; static final String SUPER_SIMPLE = "superSimple"; static final String SIMPLE_OPERATION = "simpleOperation"; @@ -39,11 +42,25 @@ class InnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE = "sampleMethodEmbeddedTypeNoRequestDetailsWithIdType"; static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; + static final String EXPAND = "expand"; + static final String OP_INSTANCE_OR_TYPE = "opInstanceOrType"; + Method getDeclaredMethod(String theMethodName, Class... theParamClasses) { + return getDeclaredMethod(this.getClass(), theMethodName, theParamClasses); + } + + Method getDeclaredMethod(@Nullable Object provider, String theMethodName, Class... theParamClasses) { + return getDeclaredMethod( + provider != null ? provider.getClass() : this.getClass(), + theMethodName, + theParamClasses); + } + + Method getDeclaredMethod(Class theContainingClass, String theMethodName, Class... theParamClasses) { try { - return this.getClass().getDeclaredMethod(theMethodName, theParamClasses); + return theContainingClass.getDeclaredMethod(theMethodName, theParamClasses); } catch (Exception exceptional) { fail(String.format("Could not find method: %s with params: %s", theMethodName, Arrays.toString(theParamClasses))); } @@ -236,11 +253,13 @@ public String toString() { } } + @Operation(name="sampleMethodEmbeddedTypeRequestDetailsFirst") String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, SampleParams theParams) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); } + @Operation(name="sampleMethodEmbeddedTypeRequestDetailsLast") String sampleMethodEmbeddedTypeRequestDetailsLast(SampleParams theParams, RequestDetails theRequestDetails) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); @@ -259,7 +278,7 @@ String sampleMethodParamNoEmbeddedType(ParamsWithoutAnnotations theParams) { String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, SampleParams theParams, RequestDetails theRequestDetails2) { // return something arbitrary - return theRequestDetails1.getId().getValue() + theParams.getParam1(); + return theRequestDetails1.getId().getValue() + theParams.getParam1() + theRequestDetails2.getId().getValue(); } String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, SampleParamsWithIdParam theParams) { @@ -279,11 +298,28 @@ MeasureReport sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWit } @Operation(name = "$expand", idempotent = true, typeName = "ValueSet") - public IBaseResource expand( + IBaseResource expand( HttpServletRequest theServletRequest, @IdParam(optional = true) IIdType theId, @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet, RequestDetails theRequestDetails) { return new Patient(); } + + // More realistic scenario where method binding actually checks the provider resource type + static class PatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Operation(name = "$OP_INSTANCE_OR_TYPE") + Parameters opInstanceOrType( + @IdParam(optional = true) IdType theId, + @OperationParam(name = "PARAM1" ) StringType theParam1, + @OperationParam(name = "PARAM2" ) Patient theParam2) { + return new Parameters(); + } + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 3062dae9ab83..4b002eb65742 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -2,10 +2,11 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,14 +20,13 @@ import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -// LUKETODO: ask #team-hdp about adding FHIR structures R4 to the test pom -// LUKETODO: test FHIR primitive types, like IntegerType, BooleanType, and IPrimitiveType -// LUKETODO: test IdParam/IIdType/etc // LUKETODO: test with RequestDetails either at the beginning or the end // LUKETODO: try to test for every case in embedded params where there's a throws @@ -61,7 +61,7 @@ void invalid_methodWithOperationParamsNoOperation() { @Test void sampleMethodOperationParams() { - final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); @@ -72,7 +72,7 @@ void sampleMethodOperationParams() { @Test void sampleMethodOperationParamsWithFhirTypes() { - final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IdType.class, String.class, List.class, BooleanType.class); + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); @@ -92,6 +92,28 @@ void sampleMethodEmbeddedParams() { // LUKETODO: assert the actual OperationEmbeddedParameter values } + @Test + void sampleMethodEmbeddedParamsRequestDetailsFirst() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + + // LUKETODO: assert the actual OperationEmbeddedParameter values + } + + @Test + void sampleMethodEmbeddedParamsRequestDetailsLast() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, RequestDetailsParameter.class); + + // LUKETODO: assert the actual OperationEmbeddedParameter values + } + @Test void sampleMethodEmbeddedParamsWithFhirTypes() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index d1508df1b636..7f0b96f54253 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.server.method; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.OP_INSTANCE_OR_TYPE; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static org.junit.jupiter.api.Assertions.*; @@ -12,23 +13,21 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.PatientProvider; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; 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; -// LUKETODO: consider whether this test needs to live here or in r4 structures -@ExtendWith(MockitoExtension.class) class OperationMethodBindingTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); @@ -38,8 +37,7 @@ class OperationMethodBindingTest { private Method myMethod; private Operation myOperation; - @Mock - private Object provider; + private Object myProvider = null; @Test void constructor_withInvalidOperationName_shouldThrowConfigurationException() { @@ -47,7 +45,7 @@ void constructor_withInvalidOperationName_shouldThrowConfigurationException() { final ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); }); assertTrue(exception.getMessage().contains("is annotated with @Operation but this annotation has no name defined")); @@ -62,7 +60,7 @@ void incomingServerRequestMatchesMethod_withMismatchedOperation_shouldReturnNone requestDetails.setRequestType(RequestTypeEnum.GET); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(requestDetails)); } @@ -76,7 +74,7 @@ void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact( requestDetails.setRequestType(RequestTypeEnum.GET); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } @@ -89,7 +87,7 @@ void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedExceptio requestDetails.setRequestType(RequestTypeEnum.PUT); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); final MethodNotAllowedException exception = assertThrows(MethodNotAllowedException.class, () -> { binding.invokeServer(null, requestDetails, new Object[]{}); @@ -109,7 +107,7 @@ void simpleMethodOperationParams() { requestDetails.setId(new IdType(ResourceType.Measure.name(), "Measure/123")); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } @@ -125,7 +123,7 @@ void simpleMethodEmbeddedParams() { requestDetails.setId(new IdType(ResourceType.Measure.name(), "Measure/123")); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } @@ -140,13 +138,29 @@ void expandEnsureMethodEnsureCanOperateAtTypeLevel() { requestDetails.setResourceName(ResourceType.ValueSet.name()); final OperationMethodBinding binding = new OperationMethodBinding( - IBaseResource.class, null, myMethod, ourFhirContext, provider, myOperation); + IBaseResource.class, null, myMethod, ourFhirContext, myProvider, myOperation); + + assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); + } + + @Test + void methodWithIdParamButNoIIdType() { + myProvider = new PatientProvider(); + init(OP_INSTANCE_OR_TYPE, IdType.class, StringType.class, Patient.class); + + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.POST); + requestDetails.setOperation("$OP_INSTANCE_OR_TYPE"); + requestDetails.setResourceName(ResourceType.Patient.name()); + + final OperationMethodBinding binding = new OperationMethodBinding( + Patient.class, Patient.class, myMethod, ourFhirContext, myProvider, myOperation); assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(requestDetails)); } private void init(String theMethodName, Class... theParamClasses) { - myMethod = myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses); + myMethod = myInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses); myOperation = myMethod.getAnnotation(Operation.class); } } From 335fb9cd6bccd1ad76cbb92fe89587bcdbe7cc40 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 20 Jan 2025 17:24:08 -0500 Subject: [PATCH 39/75] Remove restriction on multiple RequestDetails for non-embedded parameters tests. Spotless. Baby steps for refactoring MethodUtil. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 1 - ...seMethodBindingMethodParameterBuilder.java | 22 ++-- .../fhir/rest/server/method/MethodUtil.java | 113 ++++-------------- .../MethodUtilMutableLoopStateHolder.java | 16 ++- .../server/method/OperationMethodBinding.java | 7 +- 5 files changed, 50 insertions(+), 109 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 83d3e00d7baa..05751a6c04b1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -37,7 +37,6 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index d1bef4a86409..0d7e291374c5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -49,17 +49,6 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) Msg.code(234198927), theMethod, Arrays.toString(theMethodParams))); } - final Class[] methodParameterTypes = theMethod.getParameterTypes(); - - if (Arrays.stream(methodParameterTypes) - .filter(RequestDetails.class::isAssignableFrom) - .count() - > 1) { - throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", - Msg.code(924469635), theMethod.getName())); - } - final List> parameterTypesWithOperationEmbeddedParam = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( theMethod, OperationEmbeddedParam.class); @@ -74,6 +63,17 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) return theMethodParams; } + final Class[] methodParameterTypes = theMethod.getParameterTypes(); + + if (Arrays.stream(methodParameterTypes) + .filter(RequestDetails.class::isAssignableFrom) + .count() + > 1) { + throw new InternalErrorException(String.format( + "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", + Msg.code(924469635), theMethod.getName())); + } + final long numRequestDetails = Arrays.stream(methodParameterTypes) .filter(RequestDetails.class::isAssignableFrom) .count(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 43e7c0662cfb..e2373019d080 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -282,64 +282,8 @@ public static List getResourceParameters( parameterType = fieldType; - Class parameterTypeInner = parameterType; - Class> outerCollectionTypeInner = null; - Class> innerCollectionTypeInner = null; - - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionTypeInner = (Class>) parameterType; - parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); - if (parameterTypeInner == null - && methodToUse.getDeclaringClass().isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), parameterTypes); - parameterTypeInner = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - methodToUse, paramIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse - .getDeclaringClass() - .getSuperclass() + "'"); - } - } - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: now we're processing the generic parameter, so capture the inner and - // outer - // types - // Collection - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterTypeInner)) { - outerCollectionTypeInner = innerCollectionTypeInner; - innerCollectionTypeInner = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterTypeInner = ReflectionUtil.getGenericCollectionTypeOfField(field); - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: as a guard: if this is still a Collection, then throw because - // something went - // wrong - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterTypeInner)) { - throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse - .getDeclaringClass() - .getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - final MethodUtilMutableLoopStateHolder stateHolder = doStuff(methodToUse, parameterTypes, parameterType, paramIndex, field); + final MethodUtilMutableLoopStateHolder stateHolder = + doStuff(methodToUse, parameterTypes, parameterType, paramIndex, field); parameterType = stateHolder.getParameterType(); methodToUse = stateHolder.getMethodToUse(); @@ -579,8 +523,8 @@ public Object outgoingClient(Object theObject) { || !(param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add // RequestDetails if it's last - paramContexts.add( - new MethodUtilParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); + paramContexts.add(new MethodUtilParamInitializationContext( + param, parameterType, outerCollectionType, innerCollectionType)); } if (param == null) { @@ -601,7 +545,8 @@ public Object outgoingClient(Object theObject) { return parameters; } - private static MethodUtilMutableLoopStateHolder doStuff(Method theMethod, Class[] theParameterTypes, Class theParameterType, int paramIndex, Field theField) { + private static MethodUtilMutableLoopStateHolder doStuff( + Method theMethod, Class[] theParameterTypes, Class theParameterType, int paramIndex, Field theField) { Class parameterType = theParameterType; Method methodToUse = theMethod; Class> outerCollectionType = null; @@ -610,23 +555,19 @@ private static MethodUtilMutableLoopStateHolder doStuff(Method theMethod, Class< if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - if (parameterType == null - && methodToUse.getDeclaringClass().isSynthetic()) { + if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { try { methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), theParameterTypes); + .getDeclaringClass() + .getSuperclass() + .getMethod(methodToUse.getName(), theParameterTypes); parameterType = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter( - methodToUse, paramIndex); + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse - .getDeclaringClass() - .getSuperclass() + "'"); + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse.getDeclaringClass().getSuperclass() + "'"); } } // LUKETODO: @@ -650,26 +591,23 @@ private static MethodUtilMutableLoopStateHolder doStuff(Method theMethod, Class< // wrong // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" + throw new ConfigurationException(Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() + "' in type '" - + methodToUse - .getDeclaringClass() - .getCanonicalName() + + methodToUse.getDeclaringClass().getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } // LUKETODO: do I need to worry about this: - /* + /* - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + methodToUse); + } + parameterType = newParameterType; + */ // ourLog.info( // "1234: about to initialize types: method: {}, outerCollectionType: {}, @@ -679,7 +617,8 @@ private static MethodUtilMutableLoopStateHolder doStuff(Method theMethod, Class< // innerCollectionType, // parameterType); - return new MethodUtilMutableLoopStateHolder(parameterType, methodToUse, outerCollectionType, innerCollectionType); + return new MethodUtilMutableLoopStateHolder( + parameterType, methodToUse, outerCollectionType, innerCollectionType); } @Nonnull diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java index 9fc82a4bc9a2..43cde21f404f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java @@ -11,7 +11,11 @@ class MethodUtilMutableLoopStateHolder { private final Class> myOuterCollectionType; private final Class> myInnerCollectionType; - public MethodUtilMutableLoopStateHolder(Class theParameterType, Method theMethodToUse, Class> theOuterCollectionType, Class> theInnerCollectionType) { + public MethodUtilMutableLoopStateHolder( + Class theParameterType, + Method theMethodToUse, + Class> theOuterCollectionType, + Class> theInnerCollectionType) { myParameterType = theParameterType; myMethodToUse = theMethodToUse; myOuterCollectionType = theOuterCollectionType; @@ -37,10 +41,10 @@ public Class> getInnerCollectionType() { @Override public String toString() { return new StringJoiner(", ", MethodUtilMutableLoopStateHolder.class.getSimpleName() + "[", "]") - .add("myParameterType=" + myParameterType) - .add("myMethodToUse=" + myMethodToUse) - .add("myOuterCollectionType=" + myOuterCollectionType) - .add("myInnerCollectionType=" + myInnerCollectionType) - .toString(); + .add("myParameterType=" + myParameterType) + .add("myMethodToUse=" + myMethodToUse) + .add("myOuterCollectionType=" + myOuterCollectionType) + .add("myInnerCollectionType=" + myInnerCollectionType) + .toString(); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 80f0441a4da0..dfb580903150 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -22,7 +22,6 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; @@ -480,9 +479,9 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM private Optional findIdParam(Method theMethod, int theParamIndex) { // LUKETODO: do we need to validate there's only one? return Arrays.stream(theMethod.getParameterAnnotations()[theParamIndex]) - .filter(IdParam.class::isInstance) - .map(IdParam.class::cast) - .findFirst(); + .filter(IdParam.class::isInstance) + .map(IdParam.class::cast) + .findFirst(); } @Nonnull From 53810b3d6040fff52c4f6b44d060bb9860619d01 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 21 Jan 2025 09:42:35 -0500 Subject: [PATCH 40/75] More refactoring baby steps. --- .../fhir/rest/server/method/MethodUtil.java | 40 +++++++++---------- .../MethodUtilMutableLoopStateHolder.java | 14 +++---- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index e2373019d080..aa8823bf94d9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -275,28 +275,23 @@ public static List getResourceParameters( if (fieldAnnotation instanceof IdParam) { parameters.add(new NullParameter()); } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter( - theContext, fieldAnnotation, op, (OperationEmbeddedParam) - fieldAnnotation); - - parameterType = fieldType; final MethodUtilMutableLoopStateHolder stateHolder = - doStuff(methodToUse, parameterTypes, parameterType, paramIndex, field); + doStuff(methodToUse, parameterTypes, fieldType, paramIndex, field, theContext, fieldAnnotation, op); - parameterType = stateHolder.getParameterType(); - methodToUse = stateHolder.getMethodToUse(); +// parameterType = stateHolder.getParameterType(); // LUKETODO: how to handle multiple parameters.add(param); ????? - paramContexts.add(new MethodUtilParamInitializationContext( - operationEmbeddedParameter, - stateHolder.getParameterType(), - stateHolder.getOuterCollectionType(), - stateHolder.getInnerCollectionType())); + final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( + stateHolder.getOperationEmbeddedParameter(), + stateHolder.getParameterType(), + stateHolder.getOuterCollectionType(), + stateHolder.getInnerCollectionType()); + + paramContexts.add(paramContext); // LUKETODO: nasty hack to skip the null check - param = operationEmbeddedParameter; + param = stateHolder.getOperationEmbeddedParameter(); } else { // some kind of Exception for now? } @@ -546,8 +541,13 @@ public Object outgoingClient(Object theObject) { } private static MethodUtilMutableLoopStateHolder doStuff( - Method theMethod, Class[] theParameterTypes, Class theParameterType, int paramIndex, Field theField) { - Class parameterType = theParameterType; + Method theMethod, Class[] theParameterTypes, Class theFieldType, int theParamIndex, Field theField, FhirContext theContext, Annotation theFieldAnnotation, Operation theOp) { + final OperationEmbeddedParameter operationEmbeddedParameter = + getOperationEmbeddedParameter( + theContext, theFieldAnnotation, theOp, (OperationEmbeddedParam) + theFieldAnnotation); + + Class parameterType = theFieldType; Method methodToUse = theMethod; Class> outerCollectionType = null; Class> innerCollectionType = null; @@ -563,7 +563,7 @@ private static MethodUtilMutableLoopStateHolder doStuff( .getMethod(methodToUse.getName(), theParameterTypes); parameterType = // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); + ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, theParamIndex); } catch (NoSuchMethodException e) { throw new ConfigurationException(Msg.code(400) + "A method with name '" + methodToUse.getName() + "' does not exist for super class '" @@ -591,7 +591,7 @@ private static MethodUtilMutableLoopStateHolder doStuff( // wrong // LUKETODO: could be null? if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException(Msg.code(401) + "Argument #" + paramIndex + " of Method '" + throw new ConfigurationException(Msg.code(401) + "Argument #" + theParamIndex + " of Method '" + methodToUse.getName() + "' in type '" + methodToUse.getDeclaringClass().getCanonicalName() @@ -618,7 +618,7 @@ private static MethodUtilMutableLoopStateHolder doStuff( // parameterType); return new MethodUtilMutableLoopStateHolder( - parameterType, methodToUse, outerCollectionType, innerCollectionType); + parameterType, outerCollectionType, innerCollectionType, operationEmbeddedParameter); } @Nonnull diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java index 43cde21f404f..ae04027d147c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java @@ -1,33 +1,32 @@ package ca.uhn.fhir.rest.server.method; -import java.lang.reflect.Method; import java.util.Collection; import java.util.StringJoiner; // LUKETODO: javadoc class MethodUtilMutableLoopStateHolder { private final Class myParameterType; - private final Method myMethodToUse; private final Class> myOuterCollectionType; private final Class> myInnerCollectionType; + private final OperationEmbeddedParameter myOperationEmbeddedParameter; public MethodUtilMutableLoopStateHolder( Class theParameterType, - Method theMethodToUse, Class> theOuterCollectionType, - Class> theInnerCollectionType) { + Class> theInnerCollectionType, + OperationEmbeddedParameter theOperationEmbeddedParameter) { myParameterType = theParameterType; - myMethodToUse = theMethodToUse; myOuterCollectionType = theOuterCollectionType; myInnerCollectionType = theInnerCollectionType; + myOperationEmbeddedParameter = theOperationEmbeddedParameter; } public Class getParameterType() { return myParameterType; } - public Method getMethodToUse() { - return myMethodToUse; + public OperationEmbeddedParameter getOperationEmbeddedParameter() { + return myOperationEmbeddedParameter; } public Class> getOuterCollectionType() { @@ -42,7 +41,6 @@ public Class> getInnerCollectionType() { public String toString() { return new StringJoiner(", ", MethodUtilMutableLoopStateHolder.class.getSimpleName() + "[", "]") .add("myParameterType=" + myParameterType) - .add("myMethodToUse=" + myMethodToUse) .add("myOuterCollectionType=" + myOuterCollectionType) .add("myInnerCollectionType=" + myInnerCollectionType) .toString(); From 799a007e78f0ad27341c9aff0a1987ff4edf9131 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 21 Jan 2025 10:13:36 -0500 Subject: [PATCH 41/75] Even more refactoring baby steps. Spotless. --- .../fhir/rest/server/method/MethodUtil.java | 159 ++++++++++-------- .../MethodUtilMutableLoopStateHolder.java | 37 +--- 2 files changed, 97 insertions(+), 99 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index aa8823bf94d9..49dfc1539d03 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -64,6 +64,7 @@ import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -73,12 +74,9 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -215,12 +213,7 @@ public static List getResourceParameters( + methodToUse.toGenericString()); } - // LUKETODO: try to combine into validateAndGet methods - final List> operationEmbeddedTypesInner = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); - - if (operationEmbeddedTypesInner.size() > 1) { + if (operationEmbeddedTypes.size() > 1) { // LUKETODO: error throw new ConfigurationException(String.format( "%sOnly one type with embedded params is supported for now for method: %s", @@ -234,66 +227,26 @@ public static List getResourceParameters( // METHOD!!!!!!!!!!!!!!!!!!!! ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); - final Class operationEmbeddedType = operationEmbeddedTypesInner.get(0); + final Class operationEmbeddedType = operationEmbeddedTypes.get(0); final Field[] fields = operationEmbeddedType.getDeclaredFields(); for (Field field : fields) { - final String fieldName = field.getName(); - final Class fieldType = field.getType(); - final Annotation[] fieldAnnotations = field.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, methodToUse.getName())); - } + final OuterContext outerContext = + doStuffOuter(theContext, methodToUse, op, parameterTypes, field, paramIndex); - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, methodToUse.getName())); + if (outerContext.myParamter != null) { + parameters.add(outerContext.myParamter); } - final Set annotationClassNames = Arrays.stream(fieldAnnotations) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(Collectors.toUnmodifiableSet()); - - ourLog.info( - "1234: MethodUtil: TypeWithEmbeddedParams: fieldName: {}, class: {}, fieldAnnotations: {}", - fieldName, - fieldType.getName(), - annotationClassNames); - - // This is the parameter on the field in question on the type with embedded params - // class: ex - // myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; + if (outerContext.myStateHolder != null) { + paramContexts.add(outerContext.myStateHolder.getParamContext()); - if (fieldAnnotation instanceof IdParam) { - parameters.add(new NullParameter()); - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - - final MethodUtilMutableLoopStateHolder stateHolder = - doStuff(methodToUse, parameterTypes, fieldType, paramIndex, field, theContext, fieldAnnotation, op); - -// parameterType = stateHolder.getParameterType(); - - // LUKETODO: how to handle multiple parameters.add(param); ????? - final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( - stateHolder.getOperationEmbeddedParameter(), - stateHolder.getParameterType(), - stateHolder.getOuterCollectionType(), - stateHolder.getInnerCollectionType()); - - paramContexts.add(paramContext); - - // LUKETODO: nasty hack to skip the null check - param = stateHolder.getOperationEmbeddedParameter(); - } else { - // some kind of Exception for now? + // // LUKETODO: nasty hack to skip the null check + param = outerContext + .myStateHolder + .getParamContext() + .getParam(); } } } @@ -540,12 +493,81 @@ public Object outgoingClient(Object theObject) { return parameters; } + private static class OuterContext { + @Nullable + private final IParameter myParamter; + + @Nullable + private final MethodUtilMutableLoopStateHolder myStateHolder; + + public OuterContext(IParameter myParamter, @Nullable MethodUtilMutableLoopStateHolder myStateHolder) { + this.myParamter = myParamter; + this.myStateHolder = myStateHolder; + } + } + + private static OuterContext doStuffOuter( + FhirContext theContext, + Method theMethodToUse, + Operation theOp, + Class[] theParameterTypes, + Field theField, + int theParamIndex) { + final String fieldName = theField.getName(); + final Class fieldType = theField.getType(); + final Annotation[] fieldAnnotations = theField.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), fieldName, theMethodToUse.getName())); + } + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, theMethodToUse.getName())); + } + + // This is the parameter on the field in question on the type with embedded params + // class: ex + // myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + return new OuterContext(new NullParameter(), null); + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + + final MethodUtilMutableLoopStateHolder stateHolder = doStuff( + theMethodToUse, + theParameterTypes, + fieldType, + theParamIndex, + theField, + theContext, + fieldAnnotation, + theOp); + + return new OuterContext(null, stateHolder); + + } else { + // some kind of Exception for now? + return new OuterContext(null, null); + } + } + private static MethodUtilMutableLoopStateHolder doStuff( - Method theMethod, Class[] theParameterTypes, Class theFieldType, int theParamIndex, Field theField, FhirContext theContext, Annotation theFieldAnnotation, Operation theOp) { - final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter( - theContext, theFieldAnnotation, theOp, (OperationEmbeddedParam) - theFieldAnnotation); + Method theMethod, + Class[] theParameterTypes, + Class theFieldType, + int theParamIndex, + Field theField, + FhirContext theContext, + Annotation theFieldAnnotation, + Operation theOp) { + final OperationEmbeddedParameter operationEmbeddedParameter = getOperationEmbeddedParameter( + theContext, theFieldAnnotation, theOp, (OperationEmbeddedParam) theFieldAnnotation); Class parameterType = theFieldType; Method methodToUse = theMethod; @@ -616,9 +638,10 @@ private static MethodUtilMutableLoopStateHolder doStuff( // outerCollectionType, // innerCollectionType, // parameterType); + final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( + operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); - return new MethodUtilMutableLoopStateHolder( - parameterType, outerCollectionType, innerCollectionType, operationEmbeddedParameter); + return new MethodUtilMutableLoopStateHolder(paramContext); } @Nonnull diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java index ae04027d147c..c7bc1c46d5d5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java @@ -1,48 +1,23 @@ package ca.uhn.fhir.rest.server.method; -import java.util.Collection; import java.util.StringJoiner; // LUKETODO: javadoc class MethodUtilMutableLoopStateHolder { - private final Class myParameterType; - private final Class> myOuterCollectionType; - private final Class> myInnerCollectionType; - private final OperationEmbeddedParameter myOperationEmbeddedParameter; + private final MethodUtilParamInitializationContext myParamContext; - public MethodUtilMutableLoopStateHolder( - Class theParameterType, - Class> theOuterCollectionType, - Class> theInnerCollectionType, - OperationEmbeddedParameter theOperationEmbeddedParameter) { - myParameterType = theParameterType; - myOuterCollectionType = theOuterCollectionType; - myInnerCollectionType = theInnerCollectionType; - myOperationEmbeddedParameter = theOperationEmbeddedParameter; + public MethodUtilMutableLoopStateHolder(MethodUtilParamInitializationContext theParamContext) { + myParamContext = theParamContext; } - public Class getParameterType() { - return myParameterType; - } - - public OperationEmbeddedParameter getOperationEmbeddedParameter() { - return myOperationEmbeddedParameter; - } - - public Class> getOuterCollectionType() { - return myOuterCollectionType; - } - - public Class> getInnerCollectionType() { - return myInnerCollectionType; + public MethodUtilParamInitializationContext getParamContext() { + return myParamContext; } @Override public String toString() { return new StringJoiner(", ", MethodUtilMutableLoopStateHolder.class.getSimpleName() + "[", "]") - .add("myParameterType=" + myParameterType) - .add("myOuterCollectionType=" + myOuterCollectionType) - .add("myInnerCollectionType=" + myInnerCollectionType) + .add("myParamContext=" + myParamContext) .toString(); } } From a139c3395c42216a2a1003e3562ba4838543c0c4 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 21 Jan 2025 12:41:32 -0500 Subject: [PATCH 42/75] Refactor functionality into a separate class. --- .../fhir/rest/server/method/MethodUtil.java | 215 ++---------------- .../MethodUtilForEmbeddedParameters.java | 189 +++++++++++++++ .../MethodUtilMutableLoopStateHolder.java | 1 + .../fhir/rest/server/method/OuterContext.java | 29 +++ 4 files changed, 237 insertions(+), 197 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 49dfc1539d03..89d878b051e9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -63,15 +63,12 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; @@ -198,11 +195,8 @@ public static List getResourceParameters( } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { - // LUKETODO: introduce new state objects final Operation op = methodToUse.getAnnotation(Operation.class); - // LUKETODO: this relies on new new embedded params explicitly leave OUT @OperationParam on parameters if (nextParameterAnnotations.length == 0) { - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( methodToUse, OperationEmbeddedParam.class); @@ -214,42 +208,40 @@ public static List getResourceParameters( } if (operationEmbeddedTypes.size() > 1) { - // LUKETODO: error throw new ConfigurationException(String.format( "%sOnly one type with embedded params is supported for now for method: %s", Msg.code(9999927), methodToUse.getName())); } // LUKETODO: else???? - if (!operationEmbeddedTypes - .isEmpty()) { // ensure this is the opposite so we can flip between the two - // LUKETODO: TRY TO DO AS MUCH OF THIS AS POSSIBLE WITHIN A SEPARATE - // METHOD!!!!!!!!!!!!!!!!!!!! - ourLog.info("1234: has operationEmbeddedTypes !!!!!!! method: {}", methodToUse.getName()); - - final Class operationEmbeddedType = operationEmbeddedTypes.get(0); - - final Field[] fields = operationEmbeddedType.getDeclaredFields(); + if (!operationEmbeddedTypes.isEmpty()) { + final MethodUtilForEmbeddedParameters paramsStuff = + new MethodUtilForEmbeddedParameters( + theContext, + theMethod, + op, + parameterTypes, + operationEmbeddedTypes.get(0), + paramIndex); - for (Field field : fields) { - final OuterContext outerContext = - doStuffOuter(theContext, methodToUse, op, parameterTypes, field, paramIndex); + final List outerContexts = paramsStuff.doStuffOuterOuter(); - if (outerContext.myParamter != null) { - parameters.add(outerContext.myParamter); + for (OuterContext outerContext : outerContexts) { + if (outerContext.getParamter() != null) { + parameters.add(outerContext.getParamter()); } - if (outerContext.myStateHolder != null) { - paramContexts.add(outerContext.myStateHolder.getParamContext()); + if (outerContext.getStateHolder() != null) { + paramContexts.add(outerContext.getStateHolder().getParamContext()); - // // LUKETODO: nasty hack to skip the null check + // LUKETODO: nasty hack to skip the null check param = outerContext - .myStateHolder + .getStateHolder() .getParamContext() .getParam(); } } - } + } // else these are regular } for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { @@ -492,175 +484,4 @@ public Object outgoingClient(Object theObject) { } return parameters; } - - private static class OuterContext { - @Nullable - private final IParameter myParamter; - - @Nullable - private final MethodUtilMutableLoopStateHolder myStateHolder; - - public OuterContext(IParameter myParamter, @Nullable MethodUtilMutableLoopStateHolder myStateHolder) { - this.myParamter = myParamter; - this.myStateHolder = myStateHolder; - } - } - - private static OuterContext doStuffOuter( - FhirContext theContext, - Method theMethodToUse, - Operation theOp, - Class[] theParameterTypes, - Field theField, - int theParamIndex) { - final String fieldName = theField.getName(); - final Class fieldType = theField.getType(); - final Annotation[] fieldAnnotations = theField.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, theMethodToUse.getName())); - } - - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, theMethodToUse.getName())); - } - - // This is the parameter on the field in question on the type with embedded params - // class: ex - // myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; - - if (fieldAnnotation instanceof IdParam) { - return new OuterContext(new NullParameter(), null); - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - - final MethodUtilMutableLoopStateHolder stateHolder = doStuff( - theMethodToUse, - theParameterTypes, - fieldType, - theParamIndex, - theField, - theContext, - fieldAnnotation, - theOp); - - return new OuterContext(null, stateHolder); - - } else { - // some kind of Exception for now? - return new OuterContext(null, null); - } - } - - private static MethodUtilMutableLoopStateHolder doStuff( - Method theMethod, - Class[] theParameterTypes, - Class theFieldType, - int theParamIndex, - Field theField, - FhirContext theContext, - Annotation theFieldAnnotation, - Operation theOp) { - final OperationEmbeddedParameter operationEmbeddedParameter = getOperationEmbeddedParameter( - theContext, theFieldAnnotation, theOp, (OperationEmbeddedParam) theFieldAnnotation); - - Class parameterType = theFieldType; - Method methodToUse = theMethod; - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), theParameterTypes); - parameterType = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, theParamIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse.getDeclaringClass().getSuperclass() + "'"); - } - } - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: now we're processing the generic parameter, so capture the inner and - // outer - // types - // Collection - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: as a guard: if this is still a Collection, then throw because - // something went - // wrong - // LUKETODO: could be null? - if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException(Msg.code(401) + "Argument #" + theParamIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse.getDeclaringClass().getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - // LUKETODO: do I need to worry about this: - /* - - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ - - // ourLog.info( - // "1234: about to initialize types: method: {}, outerCollectionType: {}, - // innerCollectionType: {}, parameterType: {}", - // methodToUse.getName(), - // outerCollectionType, - // innerCollectionType, - // parameterType); - final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( - operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); - - return new MethodUtilMutableLoopStateHolder(paramContext); - } - - @Nonnull - private static OperationEmbeddedParameter getOperationEmbeddedParameter( - FhirContext theContext, Annotation fieldAnnotation, Operation op, OperationEmbeddedParam operationParam) { - final Annotation[] fieldAnnotationArray = new Annotation[] {fieldAnnotation}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning - // repo - return new OperationEmbeddedParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java new file mode 100644 index 000000000000..6c180f247e45 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java @@ -0,0 +1,189 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.util.ParametersUtil; +import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +// LUKETODO: javadoc +public class MethodUtilForEmbeddedParameters { + private final FhirContext myContext; + private final Method myMethod; + private final Operation myOperation; + private final Class[] myParameterTypes; + private final Class myOperationEmbeddedType; + // LUKETODO: think carefully about whether we need this at all.. + private final int myParamIndex; + + public MethodUtilForEmbeddedParameters( + FhirContext theContext, + Method theMethod, + Operation theOperation, + Class[] theParameterTypes, + Class theOperationEmbeddedType, + int theParamIndex) { + myContext = theContext; + myMethod = theMethod; + myOperation = theOperation; + myParameterTypes = theParameterTypes; + myOperationEmbeddedType = theOperationEmbeddedType; + myParamIndex = theParamIndex; + } + + List doStuffOuterOuter() { + final List outerContexts = new ArrayList<>(); + + for (Field field : myOperationEmbeddedType.getDeclaredFields()) { + outerContexts.add(doStuffOuter(field)); + } + + return outerContexts; + } + + private OuterContext doStuffOuter( + Field theField) { + final String fieldName = theField.getName(); + final Class fieldType = theField.getType(); + final Annotation[] fieldAnnotations = theField.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), fieldName, myMethod.getName())); + } + + if (fieldAnnotations.length > 1) { + // LUKETODO: error + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, myMethod.getName())); + } + + // This is the parameter on the field in question on the type with embedded params + // class: ex + // myCount + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + return new OuterContext(new NullParameter(), null); + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + + final MethodUtilMutableLoopStateHolder stateHolder = doStuff( + fieldType, + theField, + (OperationEmbeddedParam) fieldAnnotation); + + return new OuterContext(null, stateHolder); + + } else { + // LUKETODO: Nsome kind of Exception for now? + return new OuterContext(null, null); + } + } + + private MethodUtilMutableLoopStateHolder doStuff( + Class theFieldType, + Field theField, + OperationEmbeddedParam theOperationEmbeddedParam) { + + final OperationEmbeddedParameter operationEmbeddedParameter = + getOperationEmbeddedParameter(theOperationEmbeddedParam); + + Class parameterType = theFieldType; + Method methodToUse = myMethod; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + // LUKETODO: simplify this as much as possible: + + if (Collection.class.isAssignableFrom(parameterType)) { + // LUKETODO: unsafe cast + innerCollectionType = (Class>) parameterType; + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + // LUKETODO: what is this and do we need it at all? + if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { + try { + methodToUse = methodToUse + .getDeclaringClass() + .getSuperclass() + .getMethod(methodToUse.getName(), myParameterTypes); + parameterType = + // LUKETODO: what to do here if anything? + ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, myParamIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + methodToUse.getName() + "' does not exist for super class '" + + methodToUse.getDeclaringClass().getSuperclass() + "'"); + } + } + } + // LUKETODO: null handling + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + // LUKETODO: unsafe cast + innerCollectionType = (Class>) parameterType; + // LUKETODO: come up with another method to do this for field params + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + // LUKETODO: + // declaredParameterType = parameterType; + } + // LUKETODO: as a guard: if this is still a Collection, then throw because + // something went + // wrong + // LUKETODO: null handling + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException(Msg.code(401) + "Argument #" + myParamIndex + " of Method '" + + methodToUse.getName() + + "' in type '" + + methodToUse.getDeclaringClass().getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + // LUKETODO: do I need to worry about this: + /* + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!declaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + methodToUse); + } + parameterType = newParameterType; + */ + + final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( + operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); + + return new MethodUtilMutableLoopStateHolder(paramContext); + } + + @Nonnull + private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbeddedParam operationParam) { + final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; + final String description = ParametersUtil.extractDescription(fieldAnnotationArray); + final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); + + // LUKETODO: capabilities statemenet provider + // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning + // repo + return new OperationEmbeddedParameter( + myContext, + myOperation.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java index c7bc1c46d5d5..4d1fc6c59054 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java @@ -3,6 +3,7 @@ import java.util.StringJoiner; // LUKETODO: javadoc +// LUKETODO: do we need this at all anymore? class MethodUtilMutableLoopStateHolder { private final MethodUtilParamInitializationContext myParamContext; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java new file mode 100644 index 000000000000..6f2743f6ccbd --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.rest.server.method; + +import jakarta.annotation.Nullable; + +// LUKETODO: replace with an Either? +// LUKETODO: rename +// LUKETODO: javadoc +class OuterContext { + @Nullable + private final IParameter myParamter; + + @Nullable + private final MethodUtilMutableLoopStateHolder myStateHolder; + + public OuterContext(IParameter myParamter, @Nullable MethodUtilMutableLoopStateHolder myStateHolder) { + this.myParamter = myParamter; + this.myStateHolder = myStateHolder; + } + + @Nullable + public IParameter getParamter() { + return myParamter; + } + + @Nullable + public MethodUtilMutableLoopStateHolder getStateHolder() { + return myStateHolder; + } +} From dab0dc1cdd21cf1bb8d00a1947cb5a75096f1b42 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 21 Jan 2025 13:24:22 -0500 Subject: [PATCH 43/75] Spotless. --- .../fhir/rest/server/method/MethodUtil.java | 10 ++-------- .../MethodUtilForEmbeddedParameters.java | 18 ++++++------------ 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 89d878b051e9..5d87773479ba 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -215,14 +215,8 @@ public static List getResourceParameters( // LUKETODO: else???? if (!operationEmbeddedTypes.isEmpty()) { - final MethodUtilForEmbeddedParameters paramsStuff = - new MethodUtilForEmbeddedParameters( - theContext, - theMethod, - op, - parameterTypes, - operationEmbeddedTypes.get(0), - paramIndex); + final MethodUtilForEmbeddedParameters paramsStuff = new MethodUtilForEmbeddedParameters( + theContext, theMethod, op, parameterTypes, operationEmbeddedTypes.get(0), paramIndex); final List outerContexts = paramsStuff.doStuffOuterOuter(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java index 6c180f247e45..4279a2c31eaf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java @@ -52,16 +52,14 @@ List doStuffOuterOuter() { return outerContexts; } - private OuterContext doStuffOuter( - Field theField) { + private OuterContext doStuffOuter(Field theField) { final String fieldName = theField.getName(); final Class fieldType = theField.getType(); final Annotation[] fieldAnnotations = theField.getAnnotations(); if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(9999926), fieldName, myMethod.getName())); + "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); } if (fieldAnnotations.length > 1) { @@ -80,10 +78,8 @@ private OuterContext doStuffOuter( return new OuterContext(new NullParameter(), null); } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - final MethodUtilMutableLoopStateHolder stateHolder = doStuff( - fieldType, - theField, - (OperationEmbeddedParam) fieldAnnotation); + final MethodUtilMutableLoopStateHolder stateHolder = + doStuff(fieldType, theField, (OperationEmbeddedParam) fieldAnnotation); return new OuterContext(null, stateHolder); @@ -94,12 +90,10 @@ private OuterContext doStuffOuter( } private MethodUtilMutableLoopStateHolder doStuff( - Class theFieldType, - Field theField, - OperationEmbeddedParam theOperationEmbeddedParam) { + Class theFieldType, Field theField, OperationEmbeddedParam theOperationEmbeddedParam) { final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter(theOperationEmbeddedParam); + getOperationEmbeddedParameter(theOperationEmbeddedParam); Class parameterType = theFieldType; Method methodToUse = myMethod; From 169a10d2efb9a562a0c38478780fd18f374a95e4 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 21 Jan 2025 15:03:03 -0500 Subject: [PATCH 44/75] Resolve many TODOs. More refactoring and renaming. Javadoc. Spotless. --- .../annotation/OperationEmbeddedParam.java | 9 +- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 1 - .../ca/uhn/fhir/util/ReflectionUtilTest.java | 2 +- .../method/EmbeddedParameterConverter.java | 156 +++++++++++++++ .../EmbeddedParameterConverterContext.java | 50 +++++ .../fhir/rest/server/method/MethodUtil.java | 50 ++--- .../MethodUtilForEmbeddedParameters.java | 183 ------------------ .../MethodUtilMutableLoopStateHolder.java | 24 --- .../method/OperationEmbeddedParameter.java | 7 +- .../method/OperationIdParamDetails.java | 4 +- .../fhir/rest/server/method/OuterContext.java | 29 --- ...t.java => ParamInitializationContext.java} | 8 +- ...thodBindingMethodParameterBuilderTest.java | 3 +- .../server/method/InnerClassesAndMethods.java | 4 +- .../rest/server/method/MethodUtilTest.java | 5 +- .../method/OperationMethodBindingTest.java | 2 + 16 files changed, 264 insertions(+), 273 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java rename hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/{MethodUtilParamInitializationContext.java => ParamInitializationContext.java} (83%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java index 668bb602cb70..fd881a0a5e31 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -29,12 +29,17 @@ import java.lang.annotation.Target; /** + * Used by any method parameter within an class passed to an {@link Operation} method to be converted into an + * OperationEmbeddedParameter. + *

+ * The class itself doesn't have an annotation, only its fields. + *

+ * The method parameter in the operation also is explicitly not annotated */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.PARAMETER, ElementType.FIELD}) -// LUKETODO: javadoc to make this clear that it's associated with an OperationEmbeddedParameter -// LUKETODO: get rid of a bunch of cruft like MAX_UNLIMITED public @interface OperationEmbeddedParam { + // LUKETODO: after writing the conformance code get rid of a bunch of cruft like MAX_UNLIMITED /** * Value for {@link OperationEmbeddedParam#max()} indicating no maximum diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 05751a6c04b1..4afdd593a2a5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -43,7 +43,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -// LUKETODO: consider enhancing this class with operation params stuff instead public class ReflectionUtil { public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java index 94747322e91e..913ad9514c35 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java @@ -17,7 +17,7 @@ public class ReflectionUtilTest { - // LUKETODO: add tests for + // LUKETODO: add tests for new methods @Test public void testNewInstance() { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java new file mode 100644 index 000000000000..e2268600514f --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -0,0 +1,156 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.util.ParametersUtil; +import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static org.slf4j.LoggerFactory.*; + +/** + * Leveraged by {@link MethodUtil} exclusively to convert {@link OperationEmbeddedParam} parameters for a method to + * either a {@link NullParameter} or an {@link OperationEmbeddedParam}. + */ +public class EmbeddedParameterConverter { + private static final org.slf4j.Logger ourLog = getLogger(EmbeddedParameterConverter.class); + + private final FhirContext myContext; + private final Method myMethod; + private final Operation myOperation; + private final Class[] myParameterTypes; + private final Class myOperationEmbeddedType; + + public EmbeddedParameterConverter( + FhirContext theContext, + Method theMethod, + Operation theOperation, + Class[] theParameterTypes, + Class theOperationEmbeddedType) { + myContext = theContext; + myMethod = theMethod; + myOperation = theOperation; + myParameterTypes = theParameterTypes; + myOperationEmbeddedType = theOperationEmbeddedType; + } + + List convert() { + return Arrays.stream(myOperationEmbeddedType.getDeclaredFields()) + .map(this::convertField) + .collect(Collectors.toUnmodifiableList()); + } + + private EmbeddedParameterConverterContext convertField(Field theField) { + final String fieldName = theField.getName(); + final Class fieldType = theField.getType(); + final Annotation[] fieldAnnotations = theField.getAnnotations(); + + if (fieldAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); + } + + if (fieldAnnotations.length > 1) { + throw new ConfigurationException(String.format( + "%sMore than one annotation for field: %s for method: %s", + Msg.code(999998), fieldName, myMethod.getName())); + } + + final Annotation fieldAnnotation = fieldAnnotations[0]; + + if (fieldAnnotation instanceof IdParam) { + return EmbeddedParameterConverterContext.forParameter(new NullParameter()); + } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + final ParamInitializationContext paramContext = + buildParamContext(fieldType, theField, (OperationEmbeddedParam) fieldAnnotation); + + return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); + } else { + final String error = String.format( + "%sUnsupported annotation type: %s for a class: %s with OperationEmbeddedParams which is part of method: %s: ", + Msg.code(912732197), myOperationEmbeddedType, fieldAnnotation.annotationType(), myMethod.getName()); + + throw new ConfigurationException(error); + } + } + + private ParamInitializationContext buildParamContext( + Class theFieldType, Field theField, OperationEmbeddedParam theOperationEmbeddedParam) { + + final OperationEmbeddedParameter operationEmbeddedParameter = + getOperationEmbeddedParameter(theOperationEmbeddedParam); + + Class parameterType = theFieldType; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + // Flat collection + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = unsafeCast(parameterType); + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + if (parameterType == null) { + final String error = String.format( + "%s Cannot find generic type for field: %s in class: %s for method: %s", + Msg.code(724612469), + theField.getName(), + theField.getDeclaringClass().getCanonicalName(), + myMethod.getName()); + throw new ConfigurationException(error); + } + + // Collection of a Collection: Permitted + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = unsafeCast(parameterType); + parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); + } + + // Collection of a Collection of a Collection: Prohibited + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { + final String error = String.format( + "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for method: %s", + Msg.code(724612469), + theField.getName(), + theField.getDeclaringClass().getCanonicalName(), + myMethod.getName()); + throw new ConfigurationException(error); + } + } + + // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later + + return new ParamInitializationContext( + operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); + } + + @Nonnull + private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbeddedParam operationParam) { + final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; + + return new OperationEmbeddedParameter( + myContext, + myOperation.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + ParametersUtil.extractDescription(fieldAnnotationArray), + ParametersUtil.extractExamples(fieldAnnotationArray)); + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T) theObject; + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java new file mode 100644 index 000000000000..f459a1c6cee1 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.rest.server.method; + +import jakarta.annotation.Nullable; + +import java.util.StringJoiner; + +/** + * Output of {@link EmbeddedParameterConverter} that captures either a NullParameter or a ParamInitializationContext. + */ +class EmbeddedParameterConverterContext { + + @Nullable + private final NullParameter myNullParameter; + + @Nullable + private final ParamInitializationContext myParamContext; + + public static EmbeddedParameterConverterContext forParameter(NullParameter theNullParameter) { + return new EmbeddedParameterConverterContext(theNullParameter, null); + } + + public static EmbeddedParameterConverterContext forEmbeddedContext(ParamInitializationContext theParamContext) { + return new EmbeddedParameterConverterContext(null, theParamContext); + } + + private EmbeddedParameterConverterContext( + @Nullable NullParameter theNullParameter, @Nullable ParamInitializationContext theParamContext) { + + myNullParameter = theNullParameter; + myParamContext = theParamContext; + } + + @Nullable + public NullParameter getParameter() { + return myNullParameter; + } + + @Nullable + public ParamInitializationContext getParamContext() { + return myParamContext; + } + + @Override + public String toString() { + return new StringJoiner(", ", EmbeddedParameterConverterContext.class.getSimpleName() + "[", "]") + .add("myNullParameter=" + myNullParameter) + .add("myParamContext=" + myParamContext) + .toString(); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 5d87773479ba..4370c1cddc09 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -109,9 +109,8 @@ public static List getResourceParameters( int paramIndex = 0; for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { - // LUKETODO: wrapper object for all of these IParameter param = null; - final List paramContexts = new ArrayList<>(); + final List paramContexts = new ArrayList<>(); Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -196,13 +195,15 @@ public static List getResourceParameters( param = new SearchTotalModeParameter(); } else { final Operation op = methodToUse.getAnnotation(Operation.class); + // There are no annotations on this parameter, so we check to see if the parameter class has fields + // annotated OperationEmbeddedParam if (nextParameterAnnotations.length == 0) { final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( methodToUse, OperationEmbeddedParam.class); if (op == null) { - throw new ConfigurationException(Msg.code(404) + throw new ConfigurationException(Msg.code(846192641) + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + methodToUse.toGenericString()); } @@ -213,30 +214,34 @@ public static List getResourceParameters( Msg.code(9999927), methodToUse.getName())); } - // LUKETODO: else???? if (!operationEmbeddedTypes.isEmpty()) { - final MethodUtilForEmbeddedParameters paramsStuff = new MethodUtilForEmbeddedParameters( - theContext, theMethod, op, parameterTypes, operationEmbeddedTypes.get(0), paramIndex); + final EmbeddedParameterConverter embeddedParameterConverter = new EmbeddedParameterConverter( + theContext, theMethod, op, parameterTypes, operationEmbeddedTypes.get(0)); - final List outerContexts = paramsStuff.doStuffOuterOuter(); + final List outerContexts = + embeddedParameterConverter.convert(); - for (OuterContext outerContext : outerContexts) { - if (outerContext.getParamter() != null) { - parameters.add(outerContext.getParamter()); + for (EmbeddedParameterConverterContext outerContext : outerContexts) { + if (outerContext.getParameter() != null) { + parameters.add(outerContext.getParameter()); } + final ParamInitializationContext paramContext = outerContext.getParamContext(); - if (outerContext.getStateHolder() != null) { - paramContexts.add(outerContext.getStateHolder().getParamContext()); + if (paramContext != null) { + paramContexts.add(paramContext); - // LUKETODO: nasty hack to skip the null check - param = outerContext - .getStateHolder() - .getParamContext() - .getParam(); + // N.B. This a hack used only to pass the null check below, which is crucial to the + // non-embedded params logic + param = paramContext.getParam(); } } - } // else these are regular - } + } else { + // More than likely this will result in the param == null Exception below + ourLog.warn( + "Method '{}' has no parameters with annotations. Don't know how to handle this parameter", + methodToUse.getName()); + } + } // else there are no embedded params and let execution fall to the for loop below for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; @@ -452,13 +457,12 @@ public Object outgoingClient(Object theObject) { } } - // LUKETODO: do we need this or just add conditional logic? if (paramContexts.isEmpty() || !(param instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add // RequestDetails if it's last - paramContexts.add(new MethodUtilParamInitializationContext( - param, parameterType, outerCollectionType, innerCollectionType)); + paramContexts.add( + new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } if (param == null) { @@ -469,7 +473,7 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } - for (MethodUtilParamInitializationContext paramContext : paramContexts) { + for (ParamInitializationContext paramContext : paramContexts) { paramContext.initialize(methodToUse); parameters.add(paramContext.getParam()); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java deleted file mode 100644 index 4279a2c31eaf..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilForEmbeddedParameters.java +++ /dev/null @@ -1,183 +0,0 @@ -package ca.uhn.fhir.rest.server.method; - -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import ca.uhn.fhir.util.ParametersUtil; -import ca.uhn.fhir.util.ReflectionUtil; -import jakarta.annotation.Nonnull; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -// LUKETODO: javadoc -public class MethodUtilForEmbeddedParameters { - private final FhirContext myContext; - private final Method myMethod; - private final Operation myOperation; - private final Class[] myParameterTypes; - private final Class myOperationEmbeddedType; - // LUKETODO: think carefully about whether we need this at all.. - private final int myParamIndex; - - public MethodUtilForEmbeddedParameters( - FhirContext theContext, - Method theMethod, - Operation theOperation, - Class[] theParameterTypes, - Class theOperationEmbeddedType, - int theParamIndex) { - myContext = theContext; - myMethod = theMethod; - myOperation = theOperation; - myParameterTypes = theParameterTypes; - myOperationEmbeddedType = theOperationEmbeddedType; - myParamIndex = theParamIndex; - } - - List doStuffOuterOuter() { - final List outerContexts = new ArrayList<>(); - - for (Field field : myOperationEmbeddedType.getDeclaredFields()) { - outerContexts.add(doStuffOuter(field)); - } - - return outerContexts; - } - - private OuterContext doStuffOuter(Field theField) { - final String fieldName = theField.getName(); - final Class fieldType = theField.getType(); - final Annotation[] fieldAnnotations = theField.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); - } - - if (fieldAnnotations.length > 1) { - // LUKETODO: error - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, myMethod.getName())); - } - - // This is the parameter on the field in question on the type with embedded params - // class: ex - // myCount - final Annotation fieldAnnotation = fieldAnnotations[0]; - - if (fieldAnnotation instanceof IdParam) { - return new OuterContext(new NullParameter(), null); - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { - - final MethodUtilMutableLoopStateHolder stateHolder = - doStuff(fieldType, theField, (OperationEmbeddedParam) fieldAnnotation); - - return new OuterContext(null, stateHolder); - - } else { - // LUKETODO: Nsome kind of Exception for now? - return new OuterContext(null, null); - } - } - - private MethodUtilMutableLoopStateHolder doStuff( - Class theFieldType, Field theField, OperationEmbeddedParam theOperationEmbeddedParam) { - - final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter(theOperationEmbeddedParam); - - Class parameterType = theFieldType; - Method methodToUse = myMethod; - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - // LUKETODO: simplify this as much as possible: - - if (Collection.class.isAssignableFrom(parameterType)) { - // LUKETODO: unsafe cast - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - // LUKETODO: what is this and do we need it at all? - if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), myParameterTypes); - parameterType = - // LUKETODO: what to do here if anything? - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, myParamIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse.getDeclaringClass().getSuperclass() + "'"); - } - } - } - // LUKETODO: null handling - if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - // LUKETODO: unsafe cast - innerCollectionType = (Class>) parameterType; - // LUKETODO: come up with another method to do this for field params - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - // LUKETODO: - // declaredParameterType = parameterType; - } - // LUKETODO: as a guard: if this is still a Collection, then throw because - // something went - // wrong - // LUKETODO: null handling - if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException(Msg.code(401) + "Argument #" + myParamIndex + " of Method '" - + methodToUse.getName() - + "' in type '" - + methodToUse.getDeclaringClass().getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - // LUKETODO: do I need to worry about this: - /* - - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - */ - - final MethodUtilParamInitializationContext paramContext = new MethodUtilParamInitializationContext( - operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); - - return new MethodUtilMutableLoopStateHolder(paramContext); - } - - @Nonnull - private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbeddedParam operationParam) { - final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; - final String description = ParametersUtil.extractDescription(fieldAnnotationArray); - final List examples = ParametersUtil.extractExamples(fieldAnnotationArray); - - // LUKETODO: capabilities statemenet provider - // LUKETODO: consider taking ALL hapi-fhir storage-cr INTO the clinical-reasoning - // repo - return new OperationEmbeddedParameter( - myContext, - myOperation.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - } -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java deleted file mode 100644 index 4d1fc6c59054..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilMutableLoopStateHolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package ca.uhn.fhir.rest.server.method; - -import java.util.StringJoiner; - -// LUKETODO: javadoc -// LUKETODO: do we need this at all anymore? -class MethodUtilMutableLoopStateHolder { - private final MethodUtilParamInitializationContext myParamContext; - - public MethodUtilMutableLoopStateHolder(MethodUtilParamInitializationContext theParamContext) { - myParamContext = theParamContext; - } - - public MethodUtilParamInitializationContext getParamContext() { - return myParamContext; - } - - @Override - public String toString() { - return new StringJoiner(", ", MethodUtilMutableLoopStateHolder.class.getSimpleName() + "[", "]") - .add("myParamContext=" + myParamContext) - .toString(); - } -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 75b9743b713c..50eb26e42600 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -26,6 +26,8 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -51,8 +53,11 @@ import static ca.uhn.fhir.rest.server.method.OperationParameter.REQUEST_CONTENTS_USERDATA_KEY; import static org.apache.commons.lang3.StringUtils.isNotBlank; -// LUKETODO: use this for Embedded object params // LUKETODO: consider deleting whatever code may be unused +/** + * Associated with a field annotated with {@link OperationEmbeddedParam} within a class passed to a method annotated with + * {@link Operation}. + */ public class OperationEmbeddedParameter implements IParameter { // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index 59cd3ff68972..011d99464284 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -6,7 +6,9 @@ import java.util.Optional; -// LUKETODO: javadoc +/** + * This class is used to capture the details of an operation's ID parameter to be used by {@link OperationMethodBinding} + */ class OperationIdParamDetails { @Nullable diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java deleted file mode 100644 index 6f2743f6ccbd..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OuterContext.java +++ /dev/null @@ -1,29 +0,0 @@ -package ca.uhn.fhir.rest.server.method; - -import jakarta.annotation.Nullable; - -// LUKETODO: replace with an Either? -// LUKETODO: rename -// LUKETODO: javadoc -class OuterContext { - @Nullable - private final IParameter myParamter; - - @Nullable - private final MethodUtilMutableLoopStateHolder myStateHolder; - - public OuterContext(IParameter myParamter, @Nullable MethodUtilMutableLoopStateHolder myStateHolder) { - this.myParamter = myParamter; - this.myStateHolder = myStateHolder; - } - - @Nullable - public IParameter getParamter() { - return myParamter; - } - - @Nullable - public MethodUtilMutableLoopStateHolder getStateHolder() { - return myStateHolder; - } -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java similarity index 83% rename from hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java index 3410de576462..bdee8e6b4f67 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilParamInitializationContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java @@ -3,14 +3,16 @@ import java.lang.reflect.Method; import java.util.Collection; -// LUKETODO: javadoc -class MethodUtilParamInitializationContext { +/** + * Capture inputs for initializing any kind of IParameter and to defect that initialization until called. + */ +class ParamInitializationContext { private final IParameter myParam; private final Class myParameterType; private final Class> myOuterCollectionType; private final Class> myInnerCollectionType; - MethodUtilParamInitializationContext( + ParamInitializationContext( IParameter theParam, Class theParameterType, Class> theOuterCollectionType, diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index c38ad072829a..44d470bafb31 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -20,8 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -// LUKETODO: comment why this class lives in this module // LUKETODO: try to cover more InternalErrorException cases +// This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a +// circular dependency class BaseMethodBindingMethodParameterBuilderTest { // LUKETODO: assert Exception messages diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 8fffceaa58d0..24646e17f5af 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -22,12 +22,12 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.StringJoiner; import static org.junit.jupiter.api.Assertions.fail; -// LUKETODO: comment why this class lives in this module +// This class lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a +// circular dependency // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes class InnerClassesAndMethods { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 4b002eb65742..46ded8e063f6 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -27,10 +27,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -// LUKETODO: test with RequestDetails either at the beginning or the end // LUKETODO: try to test for every case in embedded params where there's a throws -// LUKETODO: comment why this class lives in this module +// This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a +// circular dependency +// LUKETODO: do we need mocks at all? @ExtendWith(MockitoExtension.class) class MethodUtilTest { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 7f0b96f54253..32ccde232f96 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -28,6 +28,8 @@ import java.lang.reflect.Method; import java.util.List; +// This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a +// circular dependency class OperationMethodBindingTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); From 7e2ce779f7a8f98034601c758661285e49a08c0d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 22 Jan 2025 10:22:38 -0500 Subject: [PATCH 45/75] Resolve many TODOs. More refactoring and renaming. Better testing. Javadoc. Spotless. --- ...seMethodBindingMethodParameterBuilder.java | 1 - .../method/EmbeddedParameterConverter.java | 1 + .../fhir/rest/server/method/MethodUtil.java | 1 - .../method/OperationEmbeddedParameter.java | 12 +- .../server/method/OperationParameter.java | 11 ++ .../r4/measure/CareGapsOperationProvider.java | 15 +- .../fhir/cr/r4/measure/CareGapsParams.java | 139 ++++++++++++++++ .../measure/EvaluateMeasureSingleParams.java | 148 ++++++++++++++++++ .../r4/measure/MeasureOperationsProvider.java | 17 +- .../docs/operatration-embedded-parameters.md | 66 ++++++++ .../rest/server/method/MethodUtilTest.java | 147 +++++++++++++++-- 11 files changed, 516 insertions(+), 42 deletions(-) create mode 100644 hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 0d7e291374c5..7bcede37df26 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -96,7 +96,6 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) theMethod, parameterTypeWithOperationEmbeddedParam, theMethodParams); } - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! private static Object[] determineMethodParamsForOperationEmbeddedParams( Method theMethod, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index e2268600514f..dfe1e2eeaf49 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -30,6 +30,7 @@ public class EmbeddedParameterConverter { private final FhirContext myContext; private final Method myMethod; private final Operation myOperation; + // LUKETODO: warning? private final Class[] myParameterTypes; private final Class myOperationEmbeddedType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4370c1cddc09..2c8b4ed4db2d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -146,7 +146,6 @@ public static List getResourceParameters( parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); declaredParameterType = parameterType; } - // LUKETODO: as a guard: if this is still a Collection, then throw because something went wrong if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 50eb26e42600..c4f176fa553d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ReflectionUtil; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; @@ -141,6 +142,11 @@ public String getParamType() { return myParamType; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + public String getSearchParamType() { if (mySearchParameterBinding != null) { return mySearchParameterBinding.getParamType().getCode(); @@ -148,6 +154,11 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + public String getOperationName() { + return myOperationName; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( @@ -197,7 +208,6 @@ public void initializeTypes( * should probably clean this up.. */ if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { - // LUKETODO: this is where we get the Exception: add an else if if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 13f11945e3fe..4e5e40f4a3f3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -49,6 +49,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ReflectionUtil; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseCoding; @@ -149,6 +150,11 @@ public String getParamType() { return myParamType; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + public String getSearchParamType() { if (mySearchParameterBinding != null) { return mySearchParameterBinding.getParamType().getCode(); @@ -156,6 +162,11 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + String getOperationName() { + return myOperationName; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 0ed9646926e6..e440d92b31e9 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -47,7 +47,6 @@ public CareGapsOperationProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } - // LUKETODO: fix javadoc /** * Implements the $care-gaps @@ -74,19 +73,7 @@ public CareGapsOperationProvider( * * @param theRequestDetails generally auto-populated by the HAPI server * framework. - * @param thePeriodStart the start of the gaps through period - * @param thePeriodEnd the end of the gaps through period - * @param theSubject a reference to either a Patient or Group for which - * the gaps in care report(s) will be generated - * @param theStatus the status code of gaps in care reports that will be - * included in the result - * @param theMeasureId the id of Measure(s) for which the gaps in care - * report(s) will be calculated - * @param theMeasureIdentifier the identifier of Measure(s) for which the gaps in - * care report(s) will be calculated - * @param theMeasureUrl the canonical URL of Measure(s) for which the gaps - * in care report(s) will be calculated - * @param theNonDocument defaults to 'false' which returns standard 'document' bundle for `$care-gaps`. + * @param theParams Please refer to the javadoc for {@link CareGapsParams} for more information on the parameters. * If 'true', this will return summarized subject bundle with only detectedIssue resource. * @return Parameters of bundles of Care Gap Measure Reports */ diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index cacb76a1a248..120669e462dd 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -5,8 +5,50 @@ import org.hl7.fhir.r4.model.CanonicalType; import java.util.List; +import java.util.Objects; import java.util.StringJoiner; +/** + * Non-RequestDetails parameters for the $care-gaps + * operation found in the + * Da Vinci DEQM + * FHIR Implementation Guide that overrides the $care-gaps + * operation found in the + * FHIR Clinical + * Reasoning Module. + *

+ * The operation calculates measures describing gaps in care. For more details, + * reference the Gaps + * in Care Reporting section of the + * Da Vinci DEQM + * FHIR Implementation Guide. + *

+ * A Parameters resource that includes zero to many document bundles that + * include Care Gap Measure Reports will be returned. + *

+ * Usage: + * URL: [base]/Measure/$care-gaps + *

+ * myRequestDetails generally auto-populated by the HAPI server + * framework. + * myPeriodStart the start of the gaps through period + * myPeriodEnd the end of the gaps through period + * mySubject a reference to either a Patient or Group for which + * the gaps in care report(s) will be generated + * myStatus the status code of gaps in care reports that will be + * included in the result + * myMeasureId the id of Measure(s) for which the gaps in care + * report(s) will be calculated + * myMeasureIdentifier the identifier of Measure(s) for which the gaps in + * care report(s) will be calculated + * myMeasureUrl the canonical URL of Measure(s) for which the gaps + * in care report(s) will be calculated + * myNonDocument defaults to 'false' which returns standard 'document' bundle for `$care-gaps`. + * If 'true', this will return summarized subject bundle with only detectedIssue resource. + */ public class CareGapsParams { @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; @@ -51,6 +93,17 @@ public CareGapsParams( this.myNonDocument = myNonDocument; } + private CareGapsParams(Builder builder) { + this.myPeriodStart = builder.myPeriodStart; + this.myPeriodEnd = builder.myPeriodEnd; + this.mySubject = builder.mySubject; + this.myStatus = builder.myStatus; + this.myMeasureId = builder.myMeasureId; + this.myMeasureIdentifier = builder.myMeasureIdentifier; + this.myMeasureUrl = builder.myMeasureUrl; + this.myNonDocument = builder.myNonDocument; + } + public String getPeriodStart() { return myPeriodStart; } @@ -83,6 +136,33 @@ public BooleanType getNonDocument() { return myNonDocument; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CareGapsParams that = (CareGapsParams) o; + return Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd) + && Objects.equals(mySubject, that.mySubject) + && Objects.equals(myStatus, that.myStatus) + && Objects.equals(myMeasureId, that.myMeasureId) + && Objects.equals(myMeasureIdentifier, that.myMeasureIdentifier) + && Objects.equals(myMeasureUrl, that.myMeasureUrl) + && Objects.equals(myNonDocument, that.myNonDocument); + } + + @Override + public int hashCode() { + return Objects.hash( + myPeriodStart, + myPeriodEnd, + mySubject, + myStatus, + myMeasureId, + myMeasureIdentifier, + myMeasureUrl, + myNonDocument); + } + @Override public String toString() { return new StringJoiner(", ", CareGapsParams.class.getSimpleName() + "[", "]") @@ -96,4 +176,63 @@ public String toString() { .add("myNonDocument=" + myNonDocument) .toString(); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String myPeriodStart; + private String myPeriodEnd; + private String mySubject; + private List myStatus; + private List myMeasureId; + private List myMeasureIdentifier; + private List myMeasureUrl; + private BooleanType myNonDocument; + + public Builder setPeriodStart(String myPeriodStart) { + this.myPeriodStart = myPeriodStart; + return this; + } + + public Builder setPeriodEnd(String myPeriodEnd) { + this.myPeriodEnd = myPeriodEnd; + return this; + } + + public Builder setSubject(String mySubject) { + this.mySubject = mySubject; + return this; + } + + public Builder setStatus(List myStatus) { + this.myStatus = myStatus; + return this; + } + + public Builder setMeasureId(List myMeasureId) { + this.myMeasureId = myMeasureId; + return this; + } + + public Builder setMeasureIdentifier(List myMeasureIdentifier) { + this.myMeasureIdentifier = myMeasureIdentifier; + return this; + } + + public Builder setMeasureUrl(List myMeasureUrl) { + this.myMeasureUrl = myMeasureUrl; + return this; + } + + public Builder setNonDocument(BooleanType myNonDocument) { + this.myNonDocument = myNonDocument; + return this; + } + + public CareGapsParams build() { + return new CareGapsParams(this); + } + } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 491baadddd2d..c94320e3781c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -7,9 +7,31 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; +import java.util.Objects; import java.util.StringJoiner; +/** + * Non-RequestDetails parameters for the $evaluate-measure + * operation found in the + * FHIR Clinical + * Reasoning Module. This implementation aims to be compatible with the CQF + * IG. + *

+ * myeId the id of the Measure to evaluate + * myPeriodStart The start of the reporting period + * myPeriodEnd The end of the reporting period + * myReportType The type of MeasureReport to generate + * mySubject the subject to use for the evaluation + * myPractitioner the practitioner to use for the evaluation + * myLastReceivedOn the date the results of this measure were last + * received. + * myProductLine the productLine (e.g. Medicare, Medicaid, etc) to use + * for the evaluation. This is a non-standard parameter. + * myAdditionalData the data bundle containing additional data + */ public class EvaluateMeasureSingleParams { + // LUKETODO: should we defined a new @IdEmbeddedParam annotation? @IdParam private final IdType myId; @@ -68,6 +90,20 @@ public EvaluateMeasureSingleParams( this.myParameters = myParameters; } + private EvaluateMeasureSingleParams(Builder builder) { + this.myId = builder.myId; + this.myPeriodStart = builder.myPeriodStart; + this.myPeriodEnd = builder.myPeriodEnd; + this.myReportType = builder.myReportType; + this.mySubject = builder.mySubject; + this.myPractitioner = builder.myPractitioner; + this.myLastReceivedOn = builder.myLastReceivedOn; + this.myProductLine = builder.myProductLine; + this.myAdditionalData = builder.myAdditionalData; + this.myTerminologyEndpoint = builder.myTerminologyEndpoint; + this.myParameters = builder.myParameters; + } + public IdType getId() { return myId; } @@ -112,6 +148,41 @@ public Parameters getParameters() { return myParameters; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + EvaluateMeasureSingleParams that = (EvaluateMeasureSingleParams) o; + return Objects.equals(myId, that.myId) + && Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd) + && Objects.equals(myReportType, that.myReportType) + && Objects.equals(mySubject, that.mySubject) + && Objects.equals(myPractitioner, that.myPractitioner) + && Objects.equals(myLastReceivedOn, that.myLastReceivedOn) + && Objects.equals(myProductLine, that.myProductLine) + && Objects.equals(myAdditionalData, that.myAdditionalData) + && Objects.equals(myTerminologyEndpoint, that.myTerminologyEndpoint) + && Objects.equals(myParameters, that.myParameters); + } + + @Override + public int hashCode() { + return Objects.hash( + myId, + myPeriodStart, + myPeriodEnd, + myReportType, + mySubject, + myPractitioner, + myLastReceivedOn, + myProductLine, + myAdditionalData, + myTerminologyEndpoint, + myParameters); + } + @Override public String toString() { return new StringJoiner(", ", EvaluateMeasureSingleParams.class.getSimpleName() + "[", "]") @@ -128,4 +199,81 @@ public String toString() { .add("myParameters=" + myParameters) .toString(); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private IdType myId; + private String myPeriodStart; + private String myPeriodEnd; + private String myReportType; + private String mySubject; + private String myPractitioner; + private String myLastReceivedOn; + private String myProductLine; + private Bundle myAdditionalData; + private Endpoint myTerminologyEndpoint; + private Parameters myParameters; + + public Builder setId(IdType myId) { + this.myId = myId; + return this; + } + + public Builder setPeriodStart(String myPeriodStart) { + this.myPeriodStart = myPeriodStart; + return this; + } + + public Builder setPeriodEnd(String myPeriodEnd) { + this.myPeriodEnd = myPeriodEnd; + return this; + } + + public Builder setReportType(String myReportType) { + this.myReportType = myReportType; + return this; + } + + public Builder setSubject(String mySubject) { + this.mySubject = mySubject; + return this; + } + + public Builder setPractitioner(String myPractitioner) { + this.myPractitioner = myPractitioner; + return this; + } + + public Builder setLastReceivedOn(String myLastReceivedOn) { + this.myLastReceivedOn = myLastReceivedOn; + return this; + } + + public Builder setProductLine(String myProductLine) { + this.myProductLine = myProductLine; + return this; + } + + public Builder setAdditionalData(Bundle myAdditionalData) { + this.myAdditionalData = myAdditionalData; + return this; + } + + public Builder setTerminologyEndpoint(Endpoint myTerminologyEndpoint) { + this.myTerminologyEndpoint = myTerminologyEndpoint; + return this; + } + + public Builder setParameters(Parameters myParameters) { + this.myParameters = myParameters; + return this; + } + + public EvaluateMeasureSingleParams build() { + return new EvaluateMeasureSingleParams(this); + } + } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 0841fd56f10d..36a9ae04bdb2 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -45,7 +45,6 @@ public MeasureOperationsProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } - // LUKETODO: fix javadoc /** * Implements the $evaluate-measure @@ -54,17 +53,7 @@ public MeasureOperationsProvider( * Reasoning Module. This implementation aims to be compatible with the CQF * IG. * - * @param theId the id of the Measure to evaluate - * @param thePeriodStart The start of the reporting period - * @param thePeriodEnd The end of the reporting period - * @param theReportType The type of MeasureReport to generate - * @param theSubject the subject to use for the evaluation - * @param thePractitioner the practitioner to use for the evaluation - * @param theLastReceivedOn the date the results of this measure were last - * received. - * @param theProductLine the productLine (e.g. Medicare, Medicaid, etc) to use - * for the evaluation. This is a non-standard parameter. - * @param theAdditionalData the data bundle containing additional data + * @param theParams Please refer to the javadoc for {@link EvaluateMeasureSingleParams} for more information on the parameters. * @param theRequestDetails The details (such as tenant) of this request. Usually * autopopulated HAPI. * @return the calculated MeasureReport @@ -79,6 +68,10 @@ public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, Requ // LUKETODO: 2. can we modify OperationParam to support the concept of mututally exclusive // params // LUKETODO: 3. code gen from operation definition + // LUKETODO: 4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation?. is there such as + // thing as a MUTUALLY EXCLUSIVE ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE + // ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation? + // so 3 different params : try annotations Eithers.forMiddle3(theParams.getId()), // LUKETODO: push this into the hapi-fhir REST framework code myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), diff --git a/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md new file mode 100644 index 000000000000..223543668d53 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md @@ -0,0 +1,66 @@ + + +# Rules + * Provider method must be annotated with @Operation + * Provider method may contain 0 to 1 RequestDetails parameters + * Provider method must contain one and only one embedded parameters class + * The method parameter for this class must NOT have any annotations, especially NOT @OperationParam + * The parameters class must not have any top-level annotations + * The parameters class must be immutable: setters will not be respected by the reflection code + * The parameters class must contain one and only one public constructor for all fields + * The parameters class may contain a Builder class for developer convenience + * The parameters class must contain at least one field annotated with @OperationEmbeddedParam + * The parameters class must contain 0 to 1 @IdParam fields, with the same rules as @IdParam variables + * The parameters class may not contain fields with any annotations other than @IdParam or @OperationEmbeddedParam + * Parameters fields otherwise follow the same rules for @IdParam and @OperationParam fields, namely the types that are allowed, including Collections and IPrimitiveType + * An @OperationEmbeddedParam is equivalent to an OperationEmbeddedParameter + * REST method parameters will be passed as separate values and converted by reflection code at runtime to a single instance of the embedded parameters class before being passed to the operation provider + +## Example + +Here is a simplified example of how this would work in practice for $evaluate-measure: + +```java +public class EvaluateMeasureSingleParams { + @IdParam + private final IdType myId; + + @OperationEmbeddedParam(name = "periodStart") + private final String myPeriodStart; + + @OperationEmbeddedParam(name = "periodEnd") + private final String myPeriodEnd; + + @OperationEmbeddedParam(name = "reportType") + private final String myReportType; + + @OperationEmbeddedParam(name = "subject") + private final String mySubject; + + public EvaluateMeasureSingleParams( + IdType myId, + String myPeriodStart, + String myPeriodEnd, + String myReportType, + String mySubject ) { + this.myId = myId; + this.myPeriodStart = myPeriodStart; + this.myPeriodEnd = myPeriodEnd; + this.myReportType = myReportType; + this.mySubject = mySubject; + } +} +``` + +```java + @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE, idempotent = true, type = Measure.class) + public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) + throws InternalErrorException, FHIRException { + return myR4MeasureServiceFactory + .create(theRequestDetails) + .evaluate(theParams); +} +``` + + + diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 46ded8e063f6..d4e1fc01dd00 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -14,7 +14,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; -import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; @@ -26,13 +27,12 @@ import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; // LUKETODO: try to test for every case in embedded params where there's a throws // This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency -// LUKETODO: do we need mocks at all? -@ExtendWith(MockitoExtension.class) class MethodUtilTest { private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); @@ -41,8 +41,7 @@ class MethodUtilTest { private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); - @Mock - private Object myProvider; + private final Object myProvider = new Object(); @Test void simpleMethodNoParams() { @@ -68,7 +67,16 @@ void sampleMethodOperationParams() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); - // LUKETODO: assert the actual OperationParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -79,7 +87,16 @@ void sampleMethodOperationParamsWithFhirTypes() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); - // LUKETODO: assert the actual OperationParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class,null), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -90,7 +107,14 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -101,7 +125,15 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new RequestDetailsParameterToAssert(), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -112,7 +144,15 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, RequestDetailsParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new RequestDetailsParameterToAssert() + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -123,7 +163,16 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -281,7 +330,79 @@ private List getMethodAndExecute(String theMethodName, Class... t myProvider); } - private List getResourceParameters(Method theMethod) { - return MethodUtil.getResourceParameters(ourFhirContext, theMethod, myProvider); + private boolean assertParametersEqual(List theExpectedParameters, List theActualParameters) { + if (theActualParameters.size() != theExpectedParameters.size()) { + fail("Expected parameters size does not match actual parameters size"); + return false; + } + + for (int i = 0; i < theActualParameters.size(); i++) { + final IParameterToAssert expectedParameter = theExpectedParameters.get(i); + final IParameter actualParameter = theActualParameters.get(i); + + if (! assertParametersEqual(expectedParameter, actualParameter)) { + return false; + } + } + + return true; + } + + private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, IParameter theActualParameter) { + if (theExpectedParameter instanceof NullParameterToAssert && theActualParameter instanceof NullParameter) { + return true; + } + + if (theExpectedParameter instanceof RequestDetailsParameterToAssert && theActualParameter instanceof RequestDetailsParameter) { + return true; + } + + if (theExpectedParameter instanceof OperationParameterToAssert expectedOperationParameter && theActualParameter instanceof OperationParameter actualOperationParameter) { + assertThat(actualOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationParameter.myContext().getVersion().getVersion()); + assertThat(actualOperationParameter.getName()).isEqualTo(expectedOperationParameter.myName()); + assertThat(actualOperationParameter.getParamType()).isEqualTo(expectedOperationParameter.myParamType()); + assertThat(actualOperationParameter.getInnerCollectionType()).isEqualTo(expectedOperationParameter.myInnerCollectionType()); + + return true; + } + + if (theExpectedParameter instanceof OperationEmbeddedParameterToAssert expectedOperationEmbeddedParameter && theActualParameter instanceof OperationEmbeddedParameter actualOperationEmbeddedParameter) { + assertThat(actualOperationEmbeddedParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationEmbeddedParameter.myContext().getVersion().getVersion()); + assertThat(actualOperationEmbeddedParameter.getName()).isEqualTo(expectedOperationEmbeddedParameter.myName()); + assertThat(actualOperationEmbeddedParameter.getParamType()).isEqualTo(expectedOperationEmbeddedParameter.myParamType()); + assertThat(actualOperationEmbeddedParameter.getInnerCollectionType()).isEqualTo(expectedOperationEmbeddedParameter.myInnerCollectionType()); + + return true; + } + + return false; + } + + private interface IParameterToAssert {} + + private record NullParameterToAssert() implements IParameterToAssert { + } + + private record RequestDetailsParameterToAssert() implements IParameterToAssert { + } + + private record OperationParameterToAssert( + FhirContext myContext, + String myName, + String myOperationName, + @SuppressWarnings("rawtypes") + Class myInnerCollectionType, + Class myParameterType, + String myParamType) implements IParameterToAssert { + } + + private record OperationEmbeddedParameterToAssert( + FhirContext myContext, + String myName, + String myOperationName, + @SuppressWarnings("rawtypes") + Class myInnerCollectionType, + Class myParameterType, + String myParamType) implements IParameterToAssert { } } From 4dc6552b50eeade0206210bdcfaa7b7cb17ec935 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 22 Jan 2025 15:21:20 -0500 Subject: [PATCH 46/75] TODOs. Exception error messages. Start trying to support ZonedDateTime in parameter conversion. Better validation of method params. --- .../annotation/OperationEmbeddedParam.java | 4 + .../rest/server/method/BaseMethodBinding.java | 6 +- ...seMethodBindingMethodParameterBuilder.java | 122 ++++---- .../server/method/EmbeddedOperationUtils.java | 113 +++++++ .../method/EmbeddedParameterConverter.java | 15 +- .../fhir/rest/server/method/MethodUtil.java | 4 +- .../method/OperationEmbeddedParameter.java | 5 +- .../server/method/OperationMethodBinding.java | 4 +- .../method}/StringTimePeriodHandler.java | 6 +- .../method}/StringTimePeriodHandlerTest.java | 2 +- .../ca/uhn/fhir/cr/config/CrBaseConfig.java | 2 +- .../ca/uhn/fhir/cr/config/r4/CrR4Config.java | 2 +- .../r4/measure/CareGapsOperationProvider.java | 5 +- .../fhir/cr/r4/measure/CareGapsParams.java | 32 +- .../measure/CollectDataOperationProvider.java | 2 +- .../measure/EvaluateMeasureSingleParams.java | 45 ++- .../measure/EvaluateMeasureSingleParams2.java | 279 ++++++++++++++++++ .../r4/measure/MeasureOperationsProvider.java | 2 +- .../docs/operatration-embedded-parameters.md | 2 + ...thodBindingMethodParameterBuilderTest.java | 22 +- .../method/EmbeddedOperationUtilsTest.java | 203 +++++++++++++ 21 files changed, 746 insertions(+), 131 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java rename {hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/common => hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method}/StringTimePeriodHandler.java (98%) rename {hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/common => hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method}/StringTimePeriodHandlerTest.java (99%) create mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java index fd881a0a5e31..8f5f8fb8b295 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -80,6 +80,10 @@ */ Class type() default IBase.class; + // LUKETODO: javadoc + // LUKETODO: Void to mean don't convert? + Class typeToConvertFrom() default Void.class; + /** * Optionally specifies the type of the parameter as a string, such as Coding or * base64Binary. This can be useful if you want to use a generic interface type diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 3a86e6444704..ed92a46f69f0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -252,7 +252,11 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final Method method = getMethod(); return method.invoke( - getProvider(), BaseMethodBindingMethodParameterBuilder.buildMethodParams(method, theMethodParams)); + getProvider(), + BaseMethodBindingMethodParameterBuilder.buildMethodParams( + method, + theMethodParams, + theRequest)); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 7bcede37df26..9cec414cf4f7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.method; +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -7,12 +8,14 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -20,22 +23,29 @@ import static java.util.function.Predicate.not; -// LUKETODO: javadoc // LUKETODO: should this be responsible for invoking the method as well? +/** + * Responsible for either passing to objects params straight through to the method call or converting them to + * fit within a class that has fields annotated with {@link OperationEmbeddedParam} and to also handle placement + * of {@link RequestDetails} in those params + */ class BaseMethodBindingMethodParameterBuilder { - private static final org.slf4j.Logger ourLog = + private static final Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + // LUKETODO: constructor param or something? + private final StringTimePeriodHandler myStringTimePeriodHandler = new StringTimePeriodHandler(ZoneOffset.UTC); + private BaseMethodBindingMethodParameterBuilder() {} - static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams) { + static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams, RequestDetails theRequestDetails) { try { return tryBuildMethodParams(theMethod, theMethodParams); - } catch (InvocationTargetException | IllegalAccessException | InstantiationException exception) { + } catch (InvocationTargetException | IllegalAccessException | InstantiationException | ConfigurationException exception) { throw new InternalErrorException( String.format( - "%s1234: Error building method params: %s", Msg.code(234198928), exception.getMessage()), + "%sError building method params: %s", Msg.code(234198928), exception.getMessage()), exception); } } @@ -55,7 +65,7 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) if (parameterTypesWithOperationEmbeddedParam.size() > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation embedded parameters. More than a single such class is part of method definition: %s", + "%sInvalid operation embedded parameters. More than a single such class is part of method definition: %s", Msg.code(924469634), theMethod.getName())); } @@ -64,14 +74,15 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) } final Class[] methodParameterTypes = theMethod.getParameterTypes(); + final String methodName = theMethod.getName(); if (Arrays.stream(methodParameterTypes) .filter(RequestDetails.class::isAssignableFrom) .count() > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", - Msg.code(924469635), theMethod.getName())); + "%sInvalid operation with embedded parameters. Cannot have more than one RequestDetails: %s", + Msg.code(924469635), methodName)); } final long numRequestDetails = Arrays.stream(methodParameterTypes) @@ -80,14 +91,14 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) if (numRequestDetails == 0 && methodParameterTypes.length > 1) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than 1 params and no RequestDetails: %s", - Msg.code(924469634), theMethod.getName())); + "%sInvalid operation with embedded parameters. Cannot have more than 1 params and no RequestDetails: %s", + Msg.code(924469634), methodName)); } if (numRequestDetails > 0 && methodParameterTypes.length > 2) { throw new InternalErrorException(String.format( - "%s1234: Invalid operation with embedded parameters. Cannot have more than 2 params and a RequestDetails: %s", - Msg.code(924469634), theMethod.getName())); + "%sInvalid operation with embedded parameters. Cannot have more than 2 params and a RequestDetails: %s", + Msg.code(924469634), methodName)); } final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); @@ -100,36 +111,40 @@ private static Object[] determineMethodParamsForOperationEmbeddedParams( Method theMethod, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { + final String methodName = theMethod.getName(); + ourLog.info( "1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", theParameterTypeWithOperationEmbeddedParam, - theMethod.getName()); + methodName); - final Object operationEmbeddedType = - buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParam, theMethodParams); + final Object operationEmbeddedType = buildOperationEmbeddedObject( + methodName, + theParameterTypeWithOperationEmbeddedParam, + theMethodParams); ourLog.info( "1234: build method params with embedded object and requestDetails (if applicable) for: {}", operationEmbeddedType); - return buildMethodParamsInCorrectPositions(theMethodParams, operationEmbeddedType); + return buildMethodParamsInCorrectPositions(methodName,theMethodParams, operationEmbeddedType); } @Nonnull private static Object buildOperationEmbeddedObject( - Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + String theMethodName, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InstantiationException, IllegalAccessException, InvocationTargetException { - final Constructor constructor = validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); + final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); - validMethodParamTypes(methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); + validMethodParamTypes(theMethodName, methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { throw new InternalErrorException(String.format( - "1234: mismatch between constructor args: %s and non-request details parameter args: %s", - Arrays.toString(constructor.getParameterTypes()), - Arrays.toString(methodParamsWithoutRequestDetails))); + "1234: mismatch between constructor args: %s and non-request details parameter args: %s", + Arrays.toString(constructor.getParameterTypes()), + Arrays.toString(methodParamsWithoutRequestDetails))); } return constructor.newInstance(methodParamsWithoutRequestDetails); @@ -139,38 +154,17 @@ private static Object buildOperationEmbeddedObject( private static Parameter[] validateAndGetConstructorParameters(Constructor constructor) { final Parameter[] constructorParameters = constructor.getParameters(); - // LUKETODO: mandate an immutable class with a constructor to set params if (constructorParameters.length == 0) { throw new InternalErrorException(Msg.code(234198927) + "No constructor that takes parameters!!!"); } return constructorParameters; } - private static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { - final Constructor[] constructors = theParameterTypeWithOperationEmbeddedParam.getConstructors(); - - if (constructors.length == 0) { - throw new InternalErrorException(String.format( - "%s1234: Invalid operation embedded parameters. Class has no constructor: %s", - Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); - } - - if (constructors.length > 1) { - throw new InternalErrorException(String.format( - "%s1234: Invalid operation embedded parameters. Class has more than one constructor: %s", - Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam)); - } - - return constructors[0]; - } - - // LUKETODO: design for future use factory methods - // RequestDetails must be dealt with separately because there is no such concept in clinical-reasoning and the // operation params classes must be defined in that project @Nonnull private static Object[] buildMethodParamsInCorrectPositions( - Object[] theMethodParams, Object operationEmbeddedType) { + String theMethodName, Object[] theMethodParams, Object operationEmbeddedType) { final List requestDetailsMultiple = Arrays.stream(theMethodParams) .filter(RequestDetails.class::isInstance) @@ -178,8 +172,12 @@ private static Object[] buildMethodParamsInCorrectPositions( .collect(Collectors.toUnmodifiableList()); if (requestDetailsMultiple.size() > 1) { - throw new InternalErrorException( - Msg.code(562462) + "1234: cannot define a request with more than one RequestDetails"); + final String error = String.format( + "%sCannot define a request with more than one RequestDetails for method: %s", + Msg.code(562462), + theMethodName); + + throw new InternalErrorException(error); } if (requestDetailsMultiple.isEmpty()) { @@ -201,18 +199,24 @@ private static Object[] buildMethodParamsInCorrectPositions( } private static void validMethodParamTypes( - Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { + String theMethodName, Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { // LUKETODO: exception message - throw new InternalErrorException(Msg.code(234198921) + "1234: bad params"); + final String error = String.format( + "%sMismatch between length of non-RequestedDetails params: %s and constructor params: %s for method: %s", + Msg.code(234198921), + methodParamsWithoutRequestDetails.length, + constructorParameters.length, + theMethodName); + throw new InternalErrorException(error); } for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { - validateMethodParamType(methodParamsWithoutRequestDetails[index], constructorParameters[index].getType()); + validateMethodParamType(theMethodName, methodParamsWithoutRequestDetails[index], constructorParameters[index].getType()); } } - private static void validateMethodParamType(Object methodParamAtIndex, Class parameterClassAtIndex) { + private static void validateMethodParamType(String theMethodName, Object methodParamAtIndex, Class parameterClassAtIndex) { if (methodParamAtIndex == null) { // argument is null, so we can't the type, so skip it: return; @@ -220,20 +224,22 @@ private static void validateMethodParamType(Object methodParamAtIndex, Class final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); - // LUKETODO: fix this this is gross if (Collection.class.isAssignableFrom(methodParamClassAtIndex) || Collection.class.isAssignableFrom(parameterClassAtIndex)) { // ex: List and ArrayList if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { - throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex)); + final String error = String.format( + "%sMismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s for method: %s", + Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex, theMethodName); + + throw new InternalErrorException(error); } // Ex: Field is declared as an IIdType, but argument is an IdDt - } else if (!parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { - throw new InternalErrorException(String.format( - "%s1234: Mismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s", - Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex)); + } else if (! parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { + final String error = String.format( + "%sMismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s for method: %s", + Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex, theMethodName); + throw new InternalErrorException(error); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java new file mode 100644 index 000000000000..284a2e494659 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -0,0 +1,113 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.i18n.Msg; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +// LUKETODO: javadoc +// LUKETODO: merge with ReflectionUtil or ParameterUtil? +// LUKETODO: think about Exceptions +public class EmbeddedOperationUtils { + + private EmbeddedOperationUtils() {} + + // LUKETODO: javadoc + static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { + final Constructor[] constructors = theParameterTypeWithOperationEmbeddedParam.getConstructors(); + + if (constructors.length == 0) { + throw new ConfigurationException(String.format( + "%sInvalid operation embedded parameters. Class has no constructor: %s", + Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); + } + + if (constructors.length > 1) { + final String error = String.format( + "%sInvalid operation embedded parameters. Class has more than one constructor: %s", + Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam); + throw new ConfigurationException(error); + } + + final Constructor soleConstructor = constructors[0]; + + validateConstructorArgs( + soleConstructor, + theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()); + + return soleConstructor; + } + + private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { + final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); + + if (constructorParameterTypes.length != theDeclaredFields.length) { + final String error = String.format( + "%sInvalid operation embedded parameters. Constructor parameter count does not match field count: %s", + Msg.code(42374927), theConstructor); + throw new ConfigurationException(error); + } + + final Type[] constructorGenericParameterTypes = theConstructor.getGenericParameterTypes(); + + for (int index = 0; index < constructorParameterTypes.length; index++) { + final Class constructorParameterTypeAtIndex = constructorParameterTypes[index]; + final Field declaredFieldAtIndex = theDeclaredFields[index]; + final Class fieldTypeAtIndex = declaredFieldAtIndex.getType(); + + if (! Modifier.isFinal(declaredFieldAtIndex.getModifiers())) { + final String error = String.format( + "%sInvalid operation embedded parameters. All fields must be final for class: %s", + Msg.code(87421741), theConstructor.getDeclaringClass()); + throw new ConfigurationException(error); + } + + if (constructorParameterTypeAtIndex != fieldTypeAtIndex) { + final String error = String.format( + "%sInvalid operation embedded parameters. Constructor parameter type does not match field type: %s", + Msg.code(87421741), theConstructor.getDeclaringClass()); + throw new ConfigurationException(error); + } + + if (Collection.class.isAssignableFrom(constructorParameterTypeAtIndex) && Collection.class.isAssignableFrom(fieldTypeAtIndex)) { + final Type constructorGenericParameterType = constructorGenericParameterTypes[index]; + final Type fieldGenericType = declaredFieldAtIndex.getGenericType(); + + validateGenericTypes(constructorGenericParameterType, fieldGenericType, theConstructor.getDeclaringClass()); + } + } + } + + private static void validateGenericTypes(Type theConstructorParameterType, Type theFieldType, Class theDeclaringClass) { + if (theConstructorParameterType instanceof ParameterizedType && theFieldType instanceof ParameterizedType) { + final ParameterizedType parameterizedParameterType = (ParameterizedType) theConstructorParameterType; + final ParameterizedType parameterizedFieldType = (ParameterizedType) theFieldType; + + final Type[] parameterTypeArguments = parameterizedParameterType.getActualTypeArguments(); + final Type[] fieldTypeArguments = parameterizedFieldType.getActualTypeArguments(); + + if (parameterTypeArguments.length != fieldTypeArguments.length) { + final String error = String.format("Generic type argument count does not match: for class: %s", theDeclaringClass); + throw new ConfigurationException(error); + } + + for (int index = 0; index < parameterTypeArguments.length; index++) { + final Type parameterTypeArgumentAtIndex = parameterTypeArguments[index]; + final Type fieldTypeArgumentAtIndex = fieldTypeArguments[index]; + + if (! parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { + final String error = String.format("Generic type argument does not match constructor: %s, field: %s for class: %s", parameterTypeArgumentAtIndex , fieldTypeArgumentAtIndex , theDeclaringClass); + throw new ConfigurationException(error); + } + } + } else { + final String error = String.format("Constructor parameter: %s or field: %s is not parameterized for class: %s", theConstructorParameterType, theFieldType, theDeclaringClass); + throw new ConfigurationException(error); + } + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index dfe1e2eeaf49..24bfc11349e9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -30,29 +30,31 @@ public class EmbeddedParameterConverter { private final FhirContext myContext; private final Method myMethod; private final Operation myOperation; - // LUKETODO: warning? - private final Class[] myParameterTypes; private final Class myOperationEmbeddedType; public EmbeddedParameterConverter( FhirContext theContext, Method theMethod, Operation theOperation, - Class[] theParameterTypes, Class theOperationEmbeddedType) { myContext = theContext; myMethod = theMethod; myOperation = theOperation; - myParameterTypes = theParameterTypes; myOperationEmbeddedType = theOperationEmbeddedType; } List convert() { - return Arrays.stream(myOperationEmbeddedType.getDeclaredFields()) + return Arrays.stream(validateConstructorArgsAndReturnFields()) .map(this::convertField) .collect(Collectors.toUnmodifiableList()); } + private Field[] validateConstructorArgsAndReturnFields() { + EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); + + return myOperationEmbeddedType.getDeclaredFields(); + } + private EmbeddedParameterConverterContext convertField(Field theField) { final String fieldName = theField.getName(); final Class fieldType = theField.getType(); @@ -147,7 +149,8 @@ private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbedd operationParam.min(), operationParam.max(), ParametersUtil.extractDescription(fieldAnnotationArray), - ParametersUtil.extractExamples(fieldAnnotationArray)); + ParametersUtil.extractExamples(fieldAnnotationArray), + operationParam.typeToConvertFrom()); } @SuppressWarnings("unchecked") diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 2c8b4ed4db2d..45ccd3f13fec 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -215,7 +215,7 @@ public static List getResourceParameters( if (!operationEmbeddedTypes.isEmpty()) { final EmbeddedParameterConverter embeddedParameterConverter = new EmbeddedParameterConverter( - theContext, theMethod, op, parameterTypes, operationEmbeddedTypes.get(0)); + theContext, theMethod, op, operationEmbeddedTypes.get(0)); final List outerContexts = embeddedParameterConverter.convert(); @@ -466,7 +466,7 @@ public Object outgoingClient(Object theObject) { if (param == null) { throw new ConfigurationException( - Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + Msg.code(408) + "Parameter #" + (paramIndex + 1) + "/" + (parameterTypes.length) + " of method '" + methodToUse.getName() + "' on type '" + methodToUse.getDeclaringClass().getCanonicalName() + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index c4f176fa553d..8f9f85037971 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -84,6 +84,7 @@ public class OperationEmbeddedParameter implements IParameter { private SearchParameter mySearchParameterBinding; private final String myDescription; private final List myExampleValues; + private final Class myTypeToConvertFrom; OperationEmbeddedParameter( FhirContext theCtx, @@ -92,13 +93,15 @@ public class OperationEmbeddedParameter implements IParameter { int theMin, int theMax, String theDescription, - List theExampleValues) { + List theExampleValues, + Class theTypeToConvertFrom) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; myMax = theMax; myContext = theCtx; myDescription = theDescription; + myTypeToConvertFrom = theTypeToConvertFrom; List exampleValues = new ArrayList<>(); if (theExampleValues != null) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index dfb580903150..5abda43338ad 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -308,8 +308,8 @@ public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequ } boolean requestHasId = theRequest.getId() != null; + ourLog.trace("method: {} has myCanOperateAtInstanceLevel : {}, requestHasId: {}", myName, myCanOperateAtInstanceLevel, requestHasId); if (requestHasId) { - ourLog.info("1234: method: {} has myCanOperateAtInstanceLevel : {}", myName, myCanOperateAtInstanceLevel); return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; } if (isNotBlank(theRequest.getResourceName())) { @@ -399,7 +399,7 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques Msg.code(428) + message, allowedRequestTypes.toArray(RequestTypeEnum[]::new)); } - ourLog.info("1234: invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); + ourLog.trace("invoking method: {} with params: {}", theRequest.getOperation(), theMethodParams); final Object response = invokeServerMethod( theRequest, myOperationIdParamDetails.alterMethodParamsIfNeeded(theRequest, theMethodParams)); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/common/StringTimePeriodHandler.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java similarity index 98% rename from hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/common/StringTimePeriodHandler.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java index 838c010718f1..df6651bf701a 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/common/StringTimePeriodHandler.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.cr.common; +package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.api.Constants; @@ -25,7 +25,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.DateUtils; import jakarta.annotation.Nullable; -import org.apache.logging.log4j.util.Strings; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -215,7 +215,7 @@ private DateTimeFormatter validateAndGetDateTimeFormat(String theInputDateTimeSt private ZoneId getClientTimezoneOrInvalidRequest(RequestDetails theRequestDetails) { final String clientTimezoneString = theRequestDetails.getHeader(Constants.HEADER_CLIENT_TIMEZONE); - if (Strings.isNotBlank(clientTimezoneString)) { + if (StringUtils.isNotBlank(clientTimezoneString)) { try { return ZoneId.of(clientTimezoneString); } catch (Exception exception) { diff --git a/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/common/StringTimePeriodHandlerTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandlerTest.java similarity index 99% rename from hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/common/StringTimePeriodHandlerTest.java rename to hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandlerTest.java index dc6c5474f5ae..549db209fd58 100644 --- a/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/common/StringTimePeriodHandlerTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandlerTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.cr.common; +package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/CrBaseConfig.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/CrBaseConfig.java index b7d85f394443..8a5cd6de353f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/CrBaseConfig.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/CrBaseConfig.java @@ -19,7 +19,7 @@ */ package ca.uhn.fhir.cr.config; -import ca.uhn.fhir.cr.common.StringTimePeriodHandler; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java index 46095d98022d..d02344c5bbdd 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.cr.common.IRepositoryFactory; import ca.uhn.fhir.cr.common.RepositoryFactoryForRepositoryInterface; -import ca.uhn.fhir.cr.common.StringTimePeriodHandler; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.config.CrBaseConfig; import ca.uhn.fhir.cr.config.ProviderLoader; import ca.uhn.fhir.cr.config.ProviderSelector; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index e440d92b31e9..e4817e41ef83 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -19,7 +19,7 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.cr.common.StringTimePeriodHandler; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.ICareGapsServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.Operation; @@ -36,7 +36,7 @@ import java.util.stream.Collectors; public class CareGapsOperationProvider { - private static final Logger ourLog = LoggerFactory.getLogger(MeasureOperationsProvider.class); + private static final Logger ourLog = LoggerFactory.getLogger(CareGapsOperationProvider.class); private final ICareGapsServiceFactory myR4CareGapsProcessorFactory; private final StringTimePeriodHandler myStringTimePeriodHandler; @@ -83,7 +83,6 @@ public CareGapsOperationProvider( "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) public Parameters careGapsReport( - // LUKETODO: do NOT use @OperationParam if this is for embedded params and document this RequestDetails theRequestDetails, CareGapsParams theParams) { return myR4CareGapsProcessorFactory diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 120669e462dd..851197be6530 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -75,22 +75,22 @@ public class CareGapsParams { private final BooleanType myNonDocument; public CareGapsParams( - String myPeriodStart, - String myPeriodEnd, - String mySubject, - List myStatus, - List myMeasureId, - List myMeasureIdentifier, - List myMeasureUrl, - BooleanType myNonDocument) { - this.myPeriodStart = myPeriodStart; - this.myPeriodEnd = myPeriodEnd; - this.mySubject = mySubject; - this.myStatus = myStatus; - this.myMeasureId = myMeasureId; - this.myMeasureIdentifier = myMeasureIdentifier; - this.myMeasureUrl = myMeasureUrl; - this.myNonDocument = myNonDocument; + String thePeriodStart, + String thePeriodEnd, + String theSubject, + List theStatus, + List theMeasureId, + List theMeasureIdentifier, + List theMeasureUrl, + BooleanType theNonDocument) { + myPeriodStart = thePeriodStart; + myPeriodEnd = thePeriodEnd; + mySubject = theSubject; + myStatus = theStatus; + myMeasureId = theMeasureId; + myMeasureIdentifier = theMeasureIdentifier; + myMeasureUrl = theMeasureUrl; + myNonDocument = theNonDocument; } private CareGapsParams(Builder builder) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java index fed4b52023ed..6fa93a640270 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java @@ -19,7 +19,7 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.cr.common.StringTimePeriodHandler; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.ICollectDataServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.IdParam; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index c94320e3781c..d6c8d9c336bd 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -31,7 +31,6 @@ * myAdditionalData the data bundle containing additional data */ public class EvaluateMeasureSingleParams { - // LUKETODO: should we defined a new @IdEmbeddedParam annotation? @IdParam private final IdType myId; @@ -66,28 +65,28 @@ public class EvaluateMeasureSingleParams { private final Parameters myParameters; public EvaluateMeasureSingleParams( - IdType myId, - String myPeriodStart, - String myPeriodEnd, - String myReportType, - String mySubject, - String myPractitioner, - String myLastReceivedOn, - String myProductLine, - Bundle myAdditionalData, - Endpoint myTerminologyEndpoint, - Parameters myParameters) { - this.myId = myId; - this.myPeriodStart = myPeriodStart; - this.myPeriodEnd = myPeriodEnd; - this.myReportType = myReportType; - this.mySubject = mySubject; - this.myPractitioner = myPractitioner; - this.myLastReceivedOn = myLastReceivedOn; - this.myProductLine = myProductLine; - this.myAdditionalData = myAdditionalData; - this.myTerminologyEndpoint = myTerminologyEndpoint; - this.myParameters = myParameters; + IdType theId, + String thePeriodStart, + String thePeriodEnd, + String theReportType, + String theSubject, + String thePractitioner, + String theLastReceivedOn, + String theProductLine, + Bundle theAdditionalData, + Endpoint theTerminologyEndpoint, + Parameters theParameters) { + myId = theId; + myPeriodStart = thePeriodStart; + myPeriodEnd = thePeriodEnd; + myReportType = theReportType; + mySubject = theSubject; + myPractitioner = thePractitioner; + myLastReceivedOn = theLastReceivedOn; + myProductLine = theProductLine; + myAdditionalData = theAdditionalData; + myTerminologyEndpoint = theTerminologyEndpoint; + myParameters = theParameters; } private EvaluateMeasureSingleParams(Builder builder) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java new file mode 100644 index 000000000000..0e0d227ff851 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java @@ -0,0 +1,279 @@ +package ca.uhn.fhir.cr.r4.measure; + +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; + +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Non-RequestDetails parameters for the $evaluate-measure + * operation found in the + * FHIR Clinical + * Reasoning Module. This implementation aims to be compatible with the CQF + * IG. + *

+ * myeId the id of the Measure to evaluate + * myPeriodStart The start of the reporting period + * myPeriodEnd The end of the reporting period + * myReportType The type of MeasureReport to generate + * mySubject the subject to use for the evaluation + * myPractitioner the practitioner to use for the evaluation + * myLastReceivedOn the date the results of this measure were last + * received. + * myProductLine the productLine (e.g. Medicare, Medicaid, etc) to use + * for the evaluation. This is a non-standard parameter. + * myAdditionalData the data bundle containing additional data + */ +public class EvaluateMeasureSingleParams2 { + @IdParam + private final IdType myId; + + @OperationEmbeddedParam(name = "periodStart", typeToConvertFrom = String.class) + private final ZonedDateTime myPeriodStart; + + @OperationEmbeddedParam(name = "periodEnd", typeToConvertFrom = String.class) + private final ZonedDateTime myPeriodEnd; + + @OperationEmbeddedParam(name = "reportType") + private final String myReportType; + + @OperationEmbeddedParam(name = "subject") + private final String mySubject; + + @OperationEmbeddedParam(name = "practitioner") + private final String myPractitioner; + + @OperationEmbeddedParam(name = "lastReceivedOn") + private final String myLastReceivedOn; + + @OperationEmbeddedParam(name = "productLine") + private final String myProductLine; + + @OperationEmbeddedParam(name = "additionalData") + private final Bundle myAdditionalData; + + @OperationEmbeddedParam(name = "terminologyEndpoint") + private final Endpoint myTerminologyEndpoint; + + @OperationEmbeddedParam(name = "parameters") + private final Parameters myParameters; + + public EvaluateMeasureSingleParams2( + IdType theId, + ZonedDateTime thePeriodStart, + ZonedDateTime thePeriodEnd, + String theReportType, + String theSubject, + String thePractitioner, + String theLastReceivedOn, + String theProductLine, + Bundle theAdditionalData, + Endpoint theTerminologyEndpoint, + Parameters theParameters) { + myId = theId; + myPeriodStart = thePeriodStart; + myPeriodEnd = thePeriodEnd; + myReportType = theReportType; + mySubject = theSubject; + myPractitioner = thePractitioner; + myLastReceivedOn = theLastReceivedOn; + myProductLine = theProductLine; + myAdditionalData = theAdditionalData; + myTerminologyEndpoint = theTerminologyEndpoint; + myParameters = theParameters; + } + + private EvaluateMeasureSingleParams2(Builder builder) { + this.myId = builder.myId; + this.myPeriodStart = builder.myPeriodStart; + this.myPeriodEnd = builder.myPeriodEnd; + this.myReportType = builder.myReportType; + this.mySubject = builder.mySubject; + this.myPractitioner = builder.myPractitioner; + this.myLastReceivedOn = builder.myLastReceivedOn; + this.myProductLine = builder.myProductLine; + this.myAdditionalData = builder.myAdditionalData; + this.myTerminologyEndpoint = builder.myTerminologyEndpoint; + this.myParameters = builder.myParameters; + } + + public IdType getId() { + return myId; + } + + public ZonedDateTime getPeriodStart() { + return myPeriodStart; + } + + public ZonedDateTime getPeriodEnd() { + return myPeriodEnd; + } + + public String getReportType() { + return myReportType; + } + + public String getSubject() { + return mySubject; + } + + public String getPractitioner() { + return myPractitioner; + } + + public String getLastReceivedOn() { + return myLastReceivedOn; + } + + public String getProductLine() { + return myProductLine; + } + + public Bundle getAdditionalData() { + return myAdditionalData; + } + + public Endpoint getTerminologyEndpoint() { + return myTerminologyEndpoint; + } + + public Parameters getParameters() { + return myParameters; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + EvaluateMeasureSingleParams2 that = (EvaluateMeasureSingleParams2) o; + return Objects.equals(myId, that.myId) + && Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd) + && Objects.equals(myReportType, that.myReportType) + && Objects.equals(mySubject, that.mySubject) + && Objects.equals(myPractitioner, that.myPractitioner) + && Objects.equals(myLastReceivedOn, that.myLastReceivedOn) + && Objects.equals(myProductLine, that.myProductLine) + && Objects.equals(myAdditionalData, that.myAdditionalData) + && Objects.equals(myTerminologyEndpoint, that.myTerminologyEndpoint) + && Objects.equals(myParameters, that.myParameters); + } + + @Override + public int hashCode() { + return Objects.hash( + myId, + myPeriodStart, + myPeriodEnd, + myReportType, + mySubject, + myPractitioner, + myLastReceivedOn, + myProductLine, + myAdditionalData, + myTerminologyEndpoint, + myParameters); + } + + @Override + public String toString() { + return new StringJoiner(", ", EvaluateMeasureSingleParams2.class.getSimpleName() + "[", "]") + .add("myId=" + myId) + .add("myPeriodStart='" + myPeriodStart + "'") + .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("myReportType='" + myReportType + "'") + .add("mySubject='" + mySubject + "'") + .add("myPractitioner='" + myPractitioner + "'") + .add("myLastReceivedOn='" + myLastReceivedOn + "'") + .add("myProductLine='" + myProductLine + "'") + .add("myAdditionalData=" + myAdditionalData) + .add("myTerminologyEndpoint=" + myTerminologyEndpoint) + .add("myParameters=" + myParameters) + .toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private IdType myId; + private ZonedDateTime myPeriodStart; + private ZonedDateTime myPeriodEnd; + private String myReportType; + private String mySubject; + private String myPractitioner; + private String myLastReceivedOn; + private String myProductLine; + private Bundle myAdditionalData; + private Endpoint myTerminologyEndpoint; + private Parameters myParameters; + + public Builder setId(IdType myId) { + this.myId = myId; + return this; + } + + public Builder setPeriodStart(ZonedDateTime myPeriodStart) { + this.myPeriodStart = myPeriodStart; + return this; + } + + public Builder setPeriodEnd(ZonedDateTime myPeriodEnd) { + this.myPeriodEnd = myPeriodEnd; + return this; + } + + public Builder setReportType(String myReportType) { + this.myReportType = myReportType; + return this; + } + + public Builder setSubject(String mySubject) { + this.mySubject = mySubject; + return this; + } + + public Builder setPractitioner(String myPractitioner) { + this.myPractitioner = myPractitioner; + return this; + } + + public Builder setLastReceivedOn(String myLastReceivedOn) { + this.myLastReceivedOn = myLastReceivedOn; + return this; + } + + public Builder setProductLine(String myProductLine) { + this.myProductLine = myProductLine; + return this; + } + + public Builder setAdditionalData(Bundle myAdditionalData) { + this.myAdditionalData = myAdditionalData; + return this; + } + + public Builder setTerminologyEndpoint(Endpoint myTerminologyEndpoint) { + this.myTerminologyEndpoint = myTerminologyEndpoint; + return this; + } + + public Builder setParameters(Parameters myParameters) { + this.myParameters = myParameters; + return this; + } + + public EvaluateMeasureSingleParams2 build() { + return new EvaluateMeasureSingleParams2(this); + } + } +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 36a9ae04bdb2..ba0145f12d42 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -19,7 +19,7 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.cr.common.StringTimePeriodHandler; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; diff --git a/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md index 223543668d53..08bfb26d9f00 100644 --- a/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md +++ b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md @@ -8,7 +8,9 @@ * The parameters class must not have any top-level annotations * The parameters class must be immutable: setters will not be respected by the reflection code * The parameters class must contain one and only one public constructor for all fields + * The arguments to the constructor must be in the same order as both the arguments passed to the REST call and the fields in the class * The parameters class may contain a Builder class for developer convenience + * In order to future-proof, the setters in the builder class must be in the same order as the fields in the class * The parameters class must contain at least one field annotated with @OperationEmbeddedParam * The parameters class must contain 0 to 1 @IdParam fields, with the same rules as @IdParam variables * The parameters class may not contain fields with any annotations other than @IdParam or @OperationEmbeddedParam diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 44d470bafb31..cfa274ea9015 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -44,7 +44,7 @@ void happyPathOperationParamsEmptyParams() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(inputParams, actualOutputParams); } @@ -54,7 +54,7 @@ void happyPathOperationParamsNonEmptyParams() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(inputParams, actualOutputParams); } @@ -65,7 +65,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -76,7 +76,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() { final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -87,7 +87,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -100,7 +100,7 @@ void happyPathOperationEmbeddedTypesWithIdType() { final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -108,7 +108,7 @@ void happyPathOperationEmbeddedTypesWithIdType() { @Test void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { assertThrows(InternalErrorException.class, () -> { - buildMethodParams(null, new Object[]{}); + buildMethodParams(null, new Object[]{}, REQUEST_DETAILS); }); } @@ -117,13 +117,13 @@ void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { - buildMethodParams(sampleMethod, null); + buildMethodParams(sampleMethod, null, REQUEST_DETAILS); }); } @Test void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException() { - assertThrows(InternalErrorException.class, () -> buildMethodParams(null, null)); + assertThrows(InternalErrorException.class, () -> buildMethodParams(null, null, null)); } @Test @@ -132,7 +132,7 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( RequestDetails.class, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { - buildMethodParams(method, inputParams); + buildMethodParams(method, inputParams, REQUEST_DETAILS); }); } @@ -145,7 +145,7 @@ void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternal final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; assertThrows(InternalErrorException.class, () -> { - buildMethodParams(method, inputParams); + buildMethodParams(method, inputParams, REQUEST_DETAILS); }); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java new file mode 100644 index 000000000000..d8df7087e291 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java @@ -0,0 +1,203 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.context.ConfigurationException; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class EmbeddedOperationUtilsTest { + private static class SimpleFieldsAndConstructorInOrder { + private final String myParam1; + private final int myParam2; + + public SimpleFieldsAndConstructorInOrder(String theParam1, int theParam2) { + myParam1 = theParam1; + myParam2 = theParam2; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SimpleFieldsAndConstructorInOrder that = (SimpleFieldsAndConstructorInOrder) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + } + + private static class SimpleFieldsAndConstructorArgLengthMismatch { + private final String myParam1; + private final int myParam2 = 1; + + public SimpleFieldsAndConstructorArgLengthMismatch (String theParam1) { + myParam1 = theParam1; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SimpleFieldsAndConstructorArgLengthMismatch that = (SimpleFieldsAndConstructorArgLengthMismatch ) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + } + + private static class SimpleFieldsAndConstructorArgOneFieldNotFinal { + private final String myParam1; + private int myParam2 = 1; + + public SimpleFieldsAndConstructorArgOneFieldNotFinal(String theParam1, int theParam2) { + myParam1 = theParam1; + myParam2 = theParam2; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SimpleFieldsAndConstructorArgOneFieldNotFinal that = (SimpleFieldsAndConstructorArgOneFieldNotFinal) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + } + + private static class SimpleFieldsAndConstructorOutOfOrder { + private final String myParam1; + private final int myParam2; + + public SimpleFieldsAndConstructorOutOfOrder(int theParam2, String theParam1) { + myParam2 = theParam2; + myParam1 = theParam1; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SimpleFieldsAndConstructorOutOfOrder that = (SimpleFieldsAndConstructorOutOfOrder) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2); + } + } + + private static class WithGenericFieldsAndConstructorInOrder { + private final String myParam1; + private final int myParam2; + private final List myParam3; + private final List myParam4; + + public WithGenericFieldsAndConstructorInOrder(String theParam1, int theParam2, List theParam3, List theParam4) { + myParam1 = theParam1; + myParam2 = theParam2; + myParam3 = theParam3; + myParam4 = theParam4; + } + + @Override + public boolean equals(Object o) { + + if (o == null || getClass() != o.getClass()) return false; + WithGenericFieldsAndConstructorInOrder that = (WithGenericFieldsAndConstructorInOrder) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam3, that.myParam3) && Objects.equals(myParam4, that.myParam4); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2, myParam3, myParam4); + } + } + + private static class WithGenericFieldsAndConstructorOutOfOrder { + private final String myParam1; + private final int myParam2; + private final List myParam3; + private final List myParam4; + + public WithGenericFieldsAndConstructorOutOfOrder(String theParam1, int theParam2, List theParam4, List theParam3) { + myParam1 = theParam1; + myParam2 = theParam2; + myParam4 = theParam4; + myParam3 = theParam3; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + WithGenericFieldsAndConstructorOutOfOrder that = (WithGenericFieldsAndConstructorOutOfOrder) o; + return myParam2 == that.myParam2 && Objects.equals(myParam1, that.myParam1) && Objects.equals(myParam3, that.myParam3) && Objects.equals(myParam4, that.myParam4); + } + + @Override + public int hashCode() { + return Objects.hash(myParam1, myParam2, myParam3, myParam4); + } + } + + @Test + void simpleConstructorInOrder() throws InvocationTargetException, InstantiationException, IllegalAccessException { + final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(SimpleFieldsAndConstructorInOrder.class); + + final String param1 = "string"; + final int param2 = 1; + + assertThat(constructor.newInstance(param1, param2)) + .isEqualTo(new SimpleFieldsAndConstructorInOrder(param1, param2)); + } + + @Test + void simpleConstructorOutOfOrder() { + assertThatThrownBy(() -> EmbeddedOperationUtils.validateAndGetConstructor(SimpleFieldsAndConstructorOutOfOrder.class)) + .isInstanceOf(ConfigurationException.class); + } + + @Test + void simpleConstructorOneFieldNotFinal() { + assertThatThrownBy(() -> EmbeddedOperationUtils.validateAndGetConstructor(SimpleFieldsAndConstructorArgOneFieldNotFinal.class)) + .isInstanceOf(ConfigurationException.class); + } + + @Test + void simpleConstructorConstructorArgMismatch() { + assertThatThrownBy(() -> EmbeddedOperationUtils.validateAndGetConstructor(SimpleFieldsAndConstructorArgLengthMismatch.class)) + .isInstanceOf(ConfigurationException.class); + } + + @Test + void withGenericsInOrder() throws InvocationTargetException, InstantiationException, IllegalAccessException { + final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(WithGenericFieldsAndConstructorInOrder.class); + + final String param1 = "string"; + final int param2 = 1; + final List param3 = List.of("string1", "string2"); + final List param4 = List.of(3, 4); + + assertThat(constructor.newInstance(param1, param2, param3, param4)) + .isEqualTo(new WithGenericFieldsAndConstructorInOrder(param1, param2, param3, param4)); + } + + @Test + void withGenericsOutOfOrder() { + assertThatThrownBy(() -> EmbeddedOperationUtils.validateAndGetConstructor(WithGenericFieldsAndConstructorOutOfOrder.class)) + .isInstanceOf(ConfigurationException.class); +// .hasMessageContaining("1234: mismatch between constructor args: [class java.lang.String, class int, class java.util.List, class java.util.List] and non-request details parameter args: [string, 1, [3, 4], [string1, string2]]"); + } +} From 0f0837fa33f0f73b56a200b724a058c20294b925 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 10:18:07 -0500 Subject: [PATCH 47/75] Barely implement ZonedDateTime conversion. --- .../EmbeddedParameterRangeType.java | 8 + .../annotation/OperationEmbeddedParam.java | 5 +- .../rest/server/method/BaseMethodBinding.java | 10 +- ...seMethodBindingMethodParameterBuilder.java | 263 +++++++++++++----- .../server/method/EmbeddedOperationUtils.java | 59 ++-- .../method/EmbeddedParameterConverter.java | 8 +- .../method/OperationEmbeddedParameter.java | 82 +++++- .../server/method/OperationMethodBinding.java | 6 +- .../ca/uhn/fhir/cr/config/r4/CrR4Config.java | 2 +- .../r4/measure/CareGapsOperationProvider.java | 5 +- .../measure/CollectDataOperationProvider.java | 2 +- .../measure/EvaluateMeasureSingleParams.java | 29 +- .../measure/EvaluateMeasureSingleParams2.java | 4 +- .../r4/measure/MeasureOperationsProvider.java | 8 +- ...thodBindingMethodParameterBuilderTest.java | 67 +++-- .../server/method/InnerClassesAndMethods.java | 52 ++++ .../rest/server/method/MethodUtilTest.java | 46 ++- 17 files changed, 490 insertions(+), 166 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java new file mode 100644 index 000000000000..d54c132885c0 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java @@ -0,0 +1,8 @@ +package ca.uhn.fhir.rest.annotation; + +// LUKETODO: javadoc +public enum EmbeddedParameterRangeType { + START, + END, + NOT_APPLICABLE +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java index 8f5f8fb8b295..632866ebc2c3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -82,7 +82,10 @@ // LUKETODO: javadoc // LUKETODO: Void to mean don't convert? - Class typeToConvertFrom() default Void.class; + Class sourceType() default Void.class; + + // LUKETODO: javadoc + EmbeddedParameterRangeType rangeType() default EmbeddedParameterRangeType.NOT_APPLICABLE; /** * Optionally specifies the type of the parameter as a string, such as Coding or diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index ed92a46f69f0..a18e2d7dc6ad 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -251,12 +251,10 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th try { final Method method = getMethod(); - return method.invoke( - getProvider(), - BaseMethodBindingMethodParameterBuilder.buildMethodParams( - method, - theMethodParams, - theRequest)); + final BaseMethodBindingMethodParameterBuilder baseMethodBindingMethodParameterBuilder = + new BaseMethodBindingMethodParameterBuilder(method, theRequest, theMethodParams); + + return method.invoke(getProvider(), baseMethodBindingMethodParameterBuilder.build()); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 9cec414cf4f7..9766c5cf8f7c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -2,24 +2,31 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static java.util.function.Predicate.not; @@ -31,50 +38,66 @@ */ class BaseMethodBindingMethodParameterBuilder { - private static final Logger ourLog = - LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); + private static final Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); // LUKETODO: constructor param or something? private final StringTimePeriodHandler myStringTimePeriodHandler = new StringTimePeriodHandler(ZoneOffset.UTC); - private BaseMethodBindingMethodParameterBuilder() {} + private final Method myMethod; + private final RequestDetails myRequestDetails; + private final Object[] myInputMethodParams; + private final Object[] myOutputMethodParams; + + BaseMethodBindingMethodParameterBuilder( + Method theMethod, RequestDetails theRequestDetails, Object[] theInputMethodParams) { + myMethod = theMethod; + myRequestDetails = theRequestDetails; + myInputMethodParams = theInputMethodParams; + myOutputMethodParams = initMethodParams(); + } + + public Object[] build() { + return myOutputMethodParams; + } - static Object[] buildMethodParams(Method theMethod, Object[] theMethodParams, RequestDetails theRequestDetails) { + private Object[] initMethodParams() { try { - return tryBuildMethodParams(theMethod, theMethodParams); - } catch (InvocationTargetException | IllegalAccessException | InstantiationException | ConfigurationException exception) { + return tryBuildMethodParams(); + } catch (InvocationTargetException + | IllegalAccessException + | InstantiationException + | ConfigurationException exception) { throw new InternalErrorException( - String.format( - "%sError building method params: %s", Msg.code(234198928), exception.getMessage()), + String.format("%sError building method params: %s", Msg.code(234198928), exception.getMessage()), exception); } } - static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) + private Object[] tryBuildMethodParams() throws InvocationTargetException, IllegalAccessException, InstantiationException { - if (theMethod == null || theMethodParams == null) { + if (myMethod == null || myInputMethodParams == null) { throw new InternalErrorException(String.format( "%s Either theMethod: %s or theMethodParams: %s is null", - Msg.code(234198927), theMethod, Arrays.toString(theMethodParams))); + Msg.code(234198927), myMethod, Arrays.toString(myInputMethodParams))); } final List> parameterTypesWithOperationEmbeddedParam = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, OperationEmbeddedParam.class); + myMethod, OperationEmbeddedParam.class); if (parameterTypesWithOperationEmbeddedParam.size() > 1) { throw new InternalErrorException(String.format( "%sInvalid operation embedded parameters. More than a single such class is part of method definition: %s", - Msg.code(924469634), theMethod.getName())); + Msg.code(924469634), myMethod.getName())); } if (parameterTypesWithOperationEmbeddedParam.isEmpty()) { - return theMethodParams; + return myInputMethodParams; } - final Class[] methodParameterTypes = theMethod.getParameterTypes(); - final String methodName = theMethod.getName(); + final Class[] methodParameterTypes = myMethod.getParameterTypes(); + final String methodName = myMethod.getName(); if (Arrays.stream(methodParameterTypes) .filter(RequestDetails.class::isAssignableFrom) @@ -103,51 +126,135 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); - return determineMethodParamsForOperationEmbeddedParams( - theMethod, parameterTypeWithOperationEmbeddedParam, theMethodParams); + return determineMethodParamsForOperationEmbeddedParams(parameterTypeWithOperationEmbeddedParam); } - private static Object[] determineMethodParamsForOperationEmbeddedParams( - Method theMethod, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + private Object[] determineMethodParamsForOperationEmbeddedParams( + Class theParameterTypeWithOperationEmbeddedParam) throws InvocationTargetException, IllegalAccessException, InstantiationException { - final String methodName = theMethod.getName(); + final String methodName = myMethod.getName(); ourLog.info( "1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", theParameterTypeWithOperationEmbeddedParam, - methodName); + methodName); final Object operationEmbeddedType = buildOperationEmbeddedObject( - methodName, - theParameterTypeWithOperationEmbeddedParam, - theMethodParams); + methodName, theParameterTypeWithOperationEmbeddedParam, myInputMethodParams); ourLog.info( "1234: build method params with embedded object and requestDetails (if applicable) for: {}", operationEmbeddedType); - return buildMethodParamsInCorrectPositions(methodName,theMethodParams, operationEmbeddedType); + return buildMethodParamsInCorrectPositions(operationEmbeddedType); } @Nonnull - private static Object buildOperationEmbeddedObject( - String theMethodName, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + private Object buildOperationEmbeddedObject( + String theMethodName, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InstantiationException, IllegalAccessException, InvocationTargetException { - final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); + final Constructor constructor = + EmbeddedOperationUtils.validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); - validMethodParamTypes(theMethodName, methodParamsWithoutRequestDetails, validateAndGetConstructorParameters(constructor)); + final Annotation[] annotations = Arrays.stream(theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()) + .map(AccessibleObject::getAnnotations) + .filter(array -> array.length == 1) + .flatMap(Arrays::stream) + .toArray(Annotation[]::new); if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { - throw new InternalErrorException(String.format( - "1234: mismatch between constructor args: %s and non-request details parameter args: %s", - Arrays.toString(constructor.getParameterTypes()), - Arrays.toString(methodParamsWithoutRequestDetails))); + final String error = String.format( + "%smismatch between constructor args: %s and non-request details parameter args: %s", + Msg.code(475326592), + Arrays.toString(constructor.getParameterTypes()), + Arrays.toString(methodParamsWithoutRequestDetails)); + throw new InternalErrorException(error); + } + + if (methodParamsWithoutRequestDetails.length != annotations.length) { + final String error = String.format( + "%smismatch between non-request details parameter args: %s and number of annotations: %s", + Msg.code(475326593), + Arrays.toString(methodParamsWithoutRequestDetails), + Arrays.toString(annotations)); + throw new InternalErrorException(error); } - return constructor.newInstance(methodParamsWithoutRequestDetails); + final Parameter[] constructorParameters = validateAndGetConstructorParameters(constructor); + + validMethodParamTypes(methodParamsWithoutRequestDetails, constructorParameters, annotations); + + final Object[] convertedParams = + convertParamsIfNeeded(methodParamsWithoutRequestDetails, constructorParameters, annotations); + + return constructor.newInstance(convertedParams); + } + + private Object[] convertParamsIfNeeded( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations) { + // LUKETODO: rangetype check? + // LUKETODO: warnings? + if (Arrays.stream(theMethodParamsWithoutRequestDetails).noneMatch(String.class::isInstance) + && Arrays.stream(theAnnotations) + .filter(OperationEmbeddedParam.class::isInstance) + .map(OperationEmbeddedParam.class::cast) + .map(OperationEmbeddedParam::sourceType) + .noneMatch(ZonedDateTime.class::isInstance)) { + + // Nothing to do: + return theMethodParamsWithoutRequestDetails; + } + + return IntStream.range(0, theMethodParamsWithoutRequestDetails.length) + .mapToObj(index -> convertParamIfNeeded( + theMethodParamsWithoutRequestDetails, theConstructorParameters, theAnnotations, index)) + .toArray(Object[]::new); + } + + @Nullable + private Object convertParamIfNeeded( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations, + int theIndex) { + + final Object paramAtIndex = theMethodParamsWithoutRequestDetails[theIndex]; + final Annotation annotation = theAnnotations[theIndex]; + + if (paramAtIndex == null) { + return paramAtIndex; + } + + if (!(annotation instanceof OperationEmbeddedParam)) { + return paramAtIndex; + } + + final OperationEmbeddedParam embeddedParamAtIndex = (OperationEmbeddedParam) annotation; + final Class paramClassAtIndex = paramAtIndex.getClass(); + final EmbeddedParameterRangeType rangeType = embeddedParamAtIndex.rangeType(); + final Parameter constructorParameter = theConstructorParameters[theIndex]; + final Class constructorParameterType = constructorParameter.getType(); + + if (EmbeddedOperationUtils.isValidSourceTypeConversion( + paramClassAtIndex, constructorParameterType, rangeType)) { + final String paramAtIndexAsString = (String) paramAtIndex; + switch (rangeType) { + case START: + return myStringTimePeriodHandler.getStartZonedDateTime(paramAtIndexAsString, myRequestDetails); + case END: + return myStringTimePeriodHandler.getEndZonedDateTime(paramAtIndexAsString, myRequestDetails); + default: + // LUKETODO: message, code, etc + throw new IllegalArgumentException(); + } + } else { + return paramAtIndex; + } } @Nonnull @@ -163,19 +270,17 @@ private static Parameter[] validateAndGetConstructorParameters(Constructor co // RequestDetails must be dealt with separately because there is no such concept in clinical-reasoning and the // operation params classes must be defined in that project @Nonnull - private static Object[] buildMethodParamsInCorrectPositions( - String theMethodName, Object[] theMethodParams, Object operationEmbeddedType) { + private Object[] buildMethodParamsInCorrectPositions(Object operationEmbeddedType) { - final List requestDetailsMultiple = Arrays.stream(theMethodParams) + final List requestDetailsMultiple = Arrays.stream(myInputMethodParams) .filter(RequestDetails.class::isInstance) .map(RequestDetails.class::cast) .collect(Collectors.toUnmodifiableList()); if (requestDetailsMultiple.size() > 1) { final String error = String.format( - "%sCannot define a request with more than one RequestDetails for method: %s", - Msg.code(562462), - theMethodName); + "%sCannot define a request with more than one RequestDetails for method: %s", + Msg.code(562462), myMethod.getName()); throw new InternalErrorException(error); } @@ -185,9 +290,10 @@ private static Object[] buildMethodParamsInCorrectPositions( return new Object[] {operationEmbeddedType}; } + // Don't try to get cute and use the RequestDetails field: just grab it from the params final RequestDetails requestDetails = requestDetailsMultiple.get(0); - final int indexOfRequestDetails = Arrays.asList(theMethodParams).indexOf(requestDetails); + final int indexOfRequestDetails = Arrays.asList(myInputMethodParams).indexOf(requestDetails); if (indexOfRequestDetails == 0) { // RequestDetails goes first @@ -198,47 +304,76 @@ private static Object[] buildMethodParamsInCorrectPositions( return new Object[] {operationEmbeddedType, requestDetails}; } - private static void validMethodParamTypes( - String theMethodName, Object[] methodParamsWithoutRequestDetails, Parameter[] constructorParameters) { - if (methodParamsWithoutRequestDetails.length != constructorParameters.length) { - // LUKETODO: exception message + private void validMethodParamTypes( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations) { + + if (theMethodParamsWithoutRequestDetails.length != theConstructorParameters.length) { final String error = String.format( - "%sMismatch between length of non-RequestedDetails params: %s and constructor params: %s for method: %s", - Msg.code(234198921), - methodParamsWithoutRequestDetails.length, - constructorParameters.length, - theMethodName); + "%sMismatch between length of non-RequestedDetails params: %s and constructor params: %s for method: %s", + Msg.code(234198921), + theMethodParamsWithoutRequestDetails.length, + theConstructorParameters.length, + myMethod.getName()); throw new InternalErrorException(error); } - for (int index = 0; index < methodParamsWithoutRequestDetails.length; index++) { - validateMethodParamType(theMethodName, methodParamsWithoutRequestDetails[index], constructorParameters[index].getType()); + for (int index = 0; index < theMethodParamsWithoutRequestDetails.length; index++) { + validateMethodParamType( + theMethodParamsWithoutRequestDetails[index], + theConstructorParameters[index].getType(), + theAnnotations[index]); } } - private static void validateMethodParamType(String theMethodName, Object methodParamAtIndex, Class parameterClassAtIndex) { - if (methodParamAtIndex == null) { + private void validateMethodParamType(Object theMethodParam, Class theParameterClass, Annotation theAnnotation) { + + if (theMethodParam == null) { // argument is null, so we can't the type, so skip it: return; } - final Class methodParamClassAtIndex = methodParamAtIndex.getClass(); + final Class methodParamClass = theMethodParam.getClass(); + + // LUKETODO: HAPI-4313421: Mismatch between methodParamClass: class org.hl7.fhir.r4.model.IdType and + // OperationEmbeddedParam source type: class java.lang.String for method: evaluateMeasure + + final Optional optOperationEmbeddedParam = + theAnnotation instanceof OperationEmbeddedParam + ? Optional.of((OperationEmbeddedParam) theAnnotation) + : Optional.empty(); + + optOperationEmbeddedParam.ifPresent(embeddedParam -> { + // LUKETODO: is this wise? + if (embeddedParam.sourceType() != Void.class && methodParamClass != embeddedParam.sourceType()) { + final String error = String.format( + "%sMismatch between methodParamClass: %s and OperationEmbeddedParam source type: %s for method: %s", + Msg.code(4313421), methodParamClass, embeddedParam.sourceType(), myMethod.getName()); + throw new InternalErrorException(error); + } + }); - if (Collection.class.isAssignableFrom(methodParamClassAtIndex) - || Collection.class.isAssignableFrom(parameterClassAtIndex)) { + if (Collection.class.isAssignableFrom(methodParamClass) + || Collection.class.isAssignableFrom(theParameterClass)) { // ex: List and ArrayList - if (methodParamClassAtIndex.isAssignableFrom(parameterClassAtIndex)) { + if (methodParamClass.isAssignableFrom(theParameterClass)) { final String error = String.format( - "%sMismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s for method: %s", - Msg.code(236146124), methodParamClassAtIndex, parameterClassAtIndex, theMethodName); + "%sMismatch between methodParamClass: %s and parameterClassAtIndex: %s for method: %s", + Msg.code(236146124), methodParamClass, theParameterClass, myMethod.getName()); throw new InternalErrorException(error); } // Ex: Field is declared as an IIdType, but argument is an IdDt - } else if (! parameterClassAtIndex.isAssignableFrom(methodParamClassAtIndex)) { + // or supported type conversion: String to ZonedDateTime + } else if (!theParameterClass.isAssignableFrom(methodParamClass) + && !optOperationEmbeddedParam + .map(embeddedParam -> EmbeddedOperationUtils.isValidSourceTypeConversion( + methodParamClass, theParameterClass, embeddedParam.rangeType())) + .orElse(false)) { final String error = String.format( - "%sMismatch between methodParamClassAtIndex: %s and parameterClassAtIndex: %s for method: %s", - Msg.code(236146125), methodParamClassAtIndex, parameterClassAtIndex, theMethodName); + "%sMismatch between methodParamClass: %s and parameterClassAtIndex: %s for method: %s", + Msg.code(236146125), methodParamClass, theParameterClass, myMethod.getName()); throw new InternalErrorException(error); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 284a2e494659..348bbbc94019 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -2,12 +2,14 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.time.ZonedDateTime; import java.util.Collection; // LUKETODO: javadoc @@ -23,33 +25,40 @@ static Constructor validateAndGetConstructor(Class theParameterTypeWithOpe if (constructors.length == 0) { throw new ConfigurationException(String.format( - "%sInvalid operation embedded parameters. Class has no constructor: %s", - Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); + "%sInvalid operation embedded parameters. Class has no constructor: %s", + Msg.code(561293645), theParameterTypeWithOperationEmbeddedParam)); } if (constructors.length > 1) { final String error = String.format( - "%sInvalid operation embedded parameters. Class has more than one constructor: %s", - Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam); + "%sInvalid operation embedded parameters. Class has more than one constructor: %s", + Msg.code(9132164), theParameterTypeWithOperationEmbeddedParam); throw new ConfigurationException(error); } final Constructor soleConstructor = constructors[0]; - validateConstructorArgs( - soleConstructor, - theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()); + validateConstructorArgs(soleConstructor, theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()); return soleConstructor; } + // LUKETODO: javadoc + // We currently only support converting from a String to a ZonedDateTime + static boolean isValidSourceTypeConversion( + Class theSourceType, Class theTargetType, EmbeddedParameterRangeType theEmbeddedParameterRangeType) { + return String.class == theSourceType + && ZonedDateTime.class == theTargetType + && EmbeddedParameterRangeType.NOT_APPLICABLE != theEmbeddedParameterRangeType; + } + private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); if (constructorParameterTypes.length != theDeclaredFields.length) { final String error = String.format( - "%sInvalid operation embedded parameters. Constructor parameter count does not match field count: %s", - Msg.code(42374927), theConstructor); + "%sInvalid operation embedded parameters. Constructor parameter count does not match field count: %s", + Msg.code(42374927), theConstructor); throw new ConfigurationException(error); } @@ -60,30 +69,33 @@ private static void validateConstructorArgs(Constructor theConstructor, Field final Field declaredFieldAtIndex = theDeclaredFields[index]; final Class fieldTypeAtIndex = declaredFieldAtIndex.getType(); - if (! Modifier.isFinal(declaredFieldAtIndex.getModifiers())) { + if (!Modifier.isFinal(declaredFieldAtIndex.getModifiers())) { final String error = String.format( - "%sInvalid operation embedded parameters. All fields must be final for class: %s", - Msg.code(87421741), theConstructor.getDeclaringClass()); + "%sInvalid operation embedded parameters. All fields must be final for class: %s", + Msg.code(87421741), theConstructor.getDeclaringClass()); throw new ConfigurationException(error); } if (constructorParameterTypeAtIndex != fieldTypeAtIndex) { final String error = String.format( - "%sInvalid operation embedded parameters. Constructor parameter type does not match field type: %s", - Msg.code(87421741), theConstructor.getDeclaringClass()); + "%sInvalid operation embedded parameters. Constructor parameter type does not match field type: %s", + Msg.code(87421741), theConstructor.getDeclaringClass()); throw new ConfigurationException(error); } - if (Collection.class.isAssignableFrom(constructorParameterTypeAtIndex) && Collection.class.isAssignableFrom(fieldTypeAtIndex)) { + if (Collection.class.isAssignableFrom(constructorParameterTypeAtIndex) + && Collection.class.isAssignableFrom(fieldTypeAtIndex)) { final Type constructorGenericParameterType = constructorGenericParameterTypes[index]; final Type fieldGenericType = declaredFieldAtIndex.getGenericType(); - validateGenericTypes(constructorGenericParameterType, fieldGenericType, theConstructor.getDeclaringClass()); + validateGenericTypes( + constructorGenericParameterType, fieldGenericType, theConstructor.getDeclaringClass()); } } } - private static void validateGenericTypes(Type theConstructorParameterType, Type theFieldType, Class theDeclaringClass) { + private static void validateGenericTypes( + Type theConstructorParameterType, Type theFieldType, Class theDeclaringClass) { if (theConstructorParameterType instanceof ParameterizedType && theFieldType instanceof ParameterizedType) { final ParameterizedType parameterizedParameterType = (ParameterizedType) theConstructorParameterType; final ParameterizedType parameterizedFieldType = (ParameterizedType) theFieldType; @@ -92,7 +104,8 @@ private static void validateGenericTypes(Type theConstructorParameterType, Type final Type[] fieldTypeArguments = parameterizedFieldType.getActualTypeArguments(); if (parameterTypeArguments.length != fieldTypeArguments.length) { - final String error = String.format("Generic type argument count does not match: for class: %s", theDeclaringClass); + final String error = + String.format("Generic type argument count does not match: for class: %s", theDeclaringClass); throw new ConfigurationException(error); } @@ -100,13 +113,17 @@ private static void validateGenericTypes(Type theConstructorParameterType, Type final Type parameterTypeArgumentAtIndex = parameterTypeArguments[index]; final Type fieldTypeArgumentAtIndex = fieldTypeArguments[index]; - if (! parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { - final String error = String.format("Generic type argument does not match constructor: %s, field: %s for class: %s", parameterTypeArgumentAtIndex , fieldTypeArgumentAtIndex , theDeclaringClass); + if (!parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { + final String error = String.format( + "Generic type argument does not match constructor: %s, field: %s for class: %s", + parameterTypeArgumentAtIndex, fieldTypeArgumentAtIndex, theDeclaringClass); throw new ConfigurationException(error); } } } else { - final String error = String.format("Constructor parameter: %s or field: %s is not parameterized for class: %s", theConstructorParameterType, theFieldType, theDeclaringClass); + final String error = String.format( + "Constructor parameter: %s or field: %s is not parameterized for class: %s", + theConstructorParameterType, theFieldType, theDeclaringClass); throw new ConfigurationException(error); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 24bfc11349e9..019a259ed812 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -33,10 +33,7 @@ public class EmbeddedParameterConverter { private final Class myOperationEmbeddedType; public EmbeddedParameterConverter( - FhirContext theContext, - Method theMethod, - Operation theOperation, - Class theOperationEmbeddedType) { + FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { myContext = theContext; myMethod = theMethod; myOperation = theOperation; @@ -150,7 +147,8 @@ private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbedd operationParam.max(), ParametersUtil.extractDescription(fieldAnnotationArray), ParametersUtil.extractExamples(fieldAnnotationArray), - operationParam.typeToConvertFrom()); + operationParam.sourceType(), + operationParam.rangeType()); } @SuppressWarnings("unchecked") diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 8f9f85037971..c37764c13e60 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -45,6 +46,8 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -60,6 +63,7 @@ * {@link Operation}. */ public class OperationEmbeddedParameter implements IParameter { + private static final Logger ourLog = LoggerFactory.getLogger(OperationEmbeddedParameter.class); // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? // LUKETODO: if so, add conditional logic everywhere to use it @@ -84,7 +88,9 @@ public class OperationEmbeddedParameter implements IParameter { private SearchParameter mySearchParameterBinding; private final String myDescription; private final List myExampleValues; - private final Class myTypeToConvertFrom; + private final Class mySourceType; + // LUKETODO: just pass the whole thing? + private final EmbeddedParameterRangeType myRengeType; OperationEmbeddedParameter( FhirContext theCtx, @@ -94,14 +100,21 @@ public class OperationEmbeddedParameter implements IParameter { int theMax, String theDescription, List theExampleValues, - Class theTypeToConvertFrom) { + Class theSourceType, + EmbeddedParameterRangeType theRengeType) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; myMax = theMax; myContext = theCtx; myDescription = theDescription; - myTypeToConvertFrom = theTypeToConvertFrom; + // LUKETODO: is this wise? + if (theSourceType == Void.class) { + mySourceType = myParameterType; + } else { + mySourceType = theSourceType; + } + myRengeType = theRengeType; List exampleValues = new ArrayList<>(); if (theExampleValues != null) { @@ -145,11 +158,6 @@ public String getParamType() { return myParamType; } - @VisibleForTesting - public Class getInnerCollectionType() { - return myInnerCollectionType; - } - public String getSearchParamType() { if (mySearchParameterBinding != null) { return mySearchParameterBinding.getParamType().getCode(); @@ -157,11 +165,21 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + @VisibleForTesting public String getOperationName() { return myOperationName; } + @VisibleForTesting + public Class getSourceType() { + return mySourceType; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( @@ -203,6 +221,7 @@ public void initializeTypes( myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) + || String.class.equals(mySourceType) || isSearchParam || ValidationModeEnum.class.equals(myParameterType); @@ -210,7 +229,9 @@ public void initializeTypes( * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We * should probably clean this up.. */ - if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { + if (!myParameterType.equals(IBase.class) + && !myParameterType.equals(String.class) + && !EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRengeType)) { if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { @@ -238,8 +259,11 @@ public void initializeTypes( myConverter = new OperationParamConverter(); } else { // LUKETODO: claim new code - throw new ConfigurationException(Msg.code(999991) + "Invalid type for @OperationParam on method " - + theMethod + ": " + myParameterType.getName()); + // LUKETODO: test the rangeType NOT_APPLICABLE scenario + final String error = String.format( + "%sInvalid type for @OperationEmbeddedParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", + Msg.code(999991), theMethod.getName(), mySourceType, myParameterType, myRengeType); + throw new ConfigurationException(error); } } } @@ -340,6 +364,12 @@ private void translateQueryParametersIntoServerArgumentForGet( } else { String[] paramValues = theRequest.getParameters().get(myName); + ourLog.info( + "1234: operation: {} paramName: {}, paramValues: {}", + theRequest.getOperation(), + myName, + Arrays.toString(paramValues)); + if (paramValues != null && paramValues.length > 0) { if (myAllowGet) { @@ -374,7 +404,9 @@ private void translateQueryParametersIntoServerArgumentForGet( matchingParamValues.add(param); }); - } else if (String.class.isAssignableFrom(myParameterType)) { + // LUKETODO: comment to explain + // LUKETODO: call EmbeddedOperationUtils.isValidSourceTypeConversion ???? + } else if (String.class.isAssignableFrom(myParameterType) || String.class.equals(mySourceType)) { matchingParamValues.addAll(Arrays.asList(paramValues)); @@ -430,6 +462,8 @@ private void processAllCommaSeparatedValues(String[] theParamValues, Consumer matchingParamValues) { + // ourLog.info("1234: translateQueryParametersIntoServerArgumentForPost: operation: {}, matchingParamValues: + // {}", theRequest.getOperation(), matchingParamValues); IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); if (requestContents != null) { RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); @@ -459,9 +493,28 @@ private void translateQueryParametersIntoServerArgumentForPost( valueChild.getAccessor().getValues(nextParameter); List paramResources = resourceChild.getAccessor().getValues(nextParameter); + ourLog.info( + "1234: try to add values: operation: {}, myName: {}, nextParameter: {}, paramValues: {} matchingParamValues: {}", + theRequest.getOperation(), + myName, + nextParameter, + paramResources, + matchingParamValues); + // try to add values: operation: $evaluate-measures, myName: periodStart, + // nextParameter: + // org.hl7.fhir.r4.model.Parameters$ParametersParameterComponent@7682050a, paramValues: + // [] matchingParamValues: [] + // LUKETODO: some part of this code reacts badly to ZonedDateTime + // HAPI-1716: Resource class[java.time.ZonedDateTime] does not contain any valid + // HAPI-FHIR annotations if (paramValues != null && !paramValues.isEmpty()) { + // paramValues non-empty: adding paramvalues: [DateType[2023-01-01]] + ourLog.info("1234: paramValues non-empty: adding paramvalues: {}", paramValues); tryToAddValues(paramValues, matchingParamValues); } else if (paramResources != null && !paramResources.isEmpty()) { + ourLog.info( + "1234: peramResources non-empty: adding peramResources: {}", + paramResources); tryToAddValues(paramResources, matchingParamValues); } } @@ -480,6 +533,7 @@ private void translateQueryParametersIntoServerArgumentForPost( @SuppressWarnings("unchecked") private void tryToAddValues(List theParamValues, List theMatchingParamValues) { + ourLog.info("1234:tryToAddValues: {}, {}", theParamValues, theMatchingParamValues); for (Object nextValue : theParamValues) { if (nextValue == null) { continue; @@ -487,7 +541,9 @@ private void tryToAddValues(List theParamValues, List theMatching if (myConverter != null) { nextValue = myConverter.incomingServer(nextValue); } - if (myParameterType.equals(String.class)) { + // LUKETODO: test this + if (myParameterType.equals(String.class) + || EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRengeType)) { if (nextValue instanceof IPrimitiveType) { IPrimitiveType source = (IPrimitiveType) nextValue; theMatchingParamValues.add(source.getValueAsString()); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 5abda43338ad..fa0aaee8c15a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -308,7 +308,11 @@ public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequ } boolean requestHasId = theRequest.getId() != null; - ourLog.trace("method: {} has myCanOperateAtInstanceLevel : {}, requestHasId: {}", myName, myCanOperateAtInstanceLevel, requestHasId); + ourLog.trace( + "method: {} has myCanOperateAtInstanceLevel : {}, requestHasId: {}", + myName, + myCanOperateAtInstanceLevel, + requestHasId); if (requestHasId) { return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java index d02344c5bbdd..6a4526dc9b9e 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.cr.common.IRepositoryFactory; import ca.uhn.fhir.cr.common.RepositoryFactoryForRepositoryInterface; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.config.CrBaseConfig; import ca.uhn.fhir.cr.config.ProviderLoader; import ca.uhn.fhir.cr.config.ProviderSelector; @@ -42,6 +41,7 @@ import ca.uhn.fhir.cr.r4.measure.MeasureOperationsProvider; import ca.uhn.fhir.cr.r4.measure.SubmitDataProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.cpg.r4.R4CqlExecutionService; import org.opencds.cqf.fhir.cr.measure.CareGapsProperties; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index e4817e41ef83..a28f4115400f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -19,11 +19,11 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.ICareGapsServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; @@ -82,8 +82,7 @@ public CareGapsOperationProvider( value = "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) - public Parameters careGapsReport( - RequestDetails theRequestDetails, CareGapsParams theParams) { + public Parameters careGapsReport(RequestDetails theRequestDetails, CareGapsParams theParams) { return myR4CareGapsProcessorFactory .create(theRequestDetails) diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java index 6fa93a640270..86c9bb4c1e87 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java @@ -19,13 +19,13 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.ICollectDataServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index d6c8d9c336bd..80e4034a9fda 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.Bundle; @@ -7,6 +8,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.StringJoiner; @@ -34,11 +36,14 @@ public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; - @OperationEmbeddedParam(name = "periodStart") - private final String myPeriodStart; + @OperationEmbeddedParam( + name = "periodStart", + sourceType = String.class, + rangeType = EmbeddedParameterRangeType.START) + private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd") - private final String myPeriodEnd; + @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + private final ZonedDateTime myPeriodEnd; @OperationEmbeddedParam(name = "reportType") private final String myReportType; @@ -66,8 +71,8 @@ public class EvaluateMeasureSingleParams { public EvaluateMeasureSingleParams( IdType theId, - String thePeriodStart, - String thePeriodEnd, + ZonedDateTime thePeriodStart, + ZonedDateTime thePeriodEnd, String theReportType, String theSubject, String thePractitioner, @@ -107,11 +112,11 @@ public IdType getId() { return myId; } - public String getPeriodStart() { + public ZonedDateTime getPeriodStart() { return myPeriodStart; } - public String getPeriodEnd() { + public ZonedDateTime getPeriodEnd() { return myPeriodEnd; } @@ -205,8 +210,8 @@ public static Builder builder() { public static class Builder { private IdType myId; - private String myPeriodStart; - private String myPeriodEnd; + private ZonedDateTime myPeriodStart; + private ZonedDateTime myPeriodEnd; private String myReportType; private String mySubject; private String myPractitioner; @@ -221,12 +226,12 @@ public Builder setId(IdType myId) { return this; } - public Builder setPeriodStart(String myPeriodStart) { + public Builder setPeriodStart(ZonedDateTime myPeriodStart) { this.myPeriodStart = myPeriodStart; return this; } - public Builder setPeriodEnd(String myPeriodEnd) { + public Builder setPeriodEnd(ZonedDateTime myPeriodEnd) { this.myPeriodEnd = myPeriodEnd; return this; } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java index 0e0d227ff851..3012af0ab011 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java @@ -35,10 +35,10 @@ public class EvaluateMeasureSingleParams2 { @IdParam private final IdType myId; - @OperationEmbeddedParam(name = "periodStart", typeToConvertFrom = String.class) + @OperationEmbeddedParam(name = "periodStart", sourceType = String.class) private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd", typeToConvertFrom = String.class) + @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class) private final ZonedDateTime myPeriodEnd; @OperationEmbeddedParam(name = "reportType") diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index ba0145f12d42..4b21b4b1ba73 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -19,11 +19,11 @@ */ package ca.uhn.fhir.cr.r4.measure; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Measure; @@ -73,10 +73,8 @@ public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, Requ // ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation? // so 3 different params : try annotations Eithers.forMiddle3(theParams.getId()), - // LUKETODO: push this into the hapi-fhir REST framework code - myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), - // LUKETODO: push this into the hapi-fhir REST framework code - myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + theParams.getPeriodStart(), + theParams.getPeriodEnd(), theParams.getReportType(), theParams.getSubject(), theParams.getLastReceivedOn(), diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index cfa274ea9015..2858978eb87f 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -13,10 +13,19 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Method; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZoneOffset; import java.util.List; -import static ca.uhn.fhir.rest.server.method.BaseMethodBindingMethodParameterBuilder.buildMethodParams; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.*; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithTypeConversion; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithoutAnnotations; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -25,10 +34,10 @@ // circular dependency class BaseMethodBindingMethodParameterBuilderTest { + // LUKETODO: test ZonedDateTime + IdParam // LUKETODO: assert Exception messages - private static final org.slf4j.Logger ourLog = - LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilderTest.class); + private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilderTest.class); private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); @@ -44,7 +53,7 @@ void happyPathOperationParamsEmptyParams() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(inputParams, actualOutputParams); } @@ -54,7 +63,7 @@ void happyPathOperationParamsNonEmptyParams() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(inputParams, actualOutputParams); } @@ -65,7 +74,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -76,18 +85,18 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() { final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @Test void happyPathOperationEmbeddedTypesRequestDetailsLast() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -96,11 +105,11 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { @Disabled void happyPathOperationEmbeddedTypesWithIdType() { final IdType id = new IdType(); - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; - final Object[] actualOutputParams = buildMethodParams(sampleMethod, inputParams, REQUEST_DETAILS); + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); assertArrayEquals(expectedOutputParams, actualOutputParams); } @@ -108,7 +117,7 @@ void happyPathOperationEmbeddedTypesWithIdType() { @Test void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { assertThrows(InternalErrorException.class, () -> { - buildMethodParams(null, new Object[]{}, REQUEST_DETAILS); + buildMethodParams(null, REQUEST_DETAILS, new Object[]{}); }); } @@ -117,7 +126,7 @@ void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { - buildMethodParams(sampleMethod, null, REQUEST_DETAILS); + buildMethodParams(sampleMethod, REQUEST_DETAILS, null); }); } @@ -128,11 +137,11 @@ void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException @Test void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, + final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, RequestDetails.class, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { - buildMethodParams(method, inputParams, REQUEST_DETAILS); + buildMethodParams(method, REQUEST_DETAILS, inputParams); }); } @@ -140,12 +149,34 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( @Test @Disabled void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); + final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; assertThrows(InternalErrorException.class, () -> { - buildMethodParams(method, inputParams, REQUEST_DETAILS); + buildMethodParams(method, REQUEST_DETAILS, inputParams); }); } + + @Test + void paramsConversionZonedDateTime() { + final Method method = myInnerClassesAndMethods.getDeclaredMethod(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); + + final Object[] inputParams = new Object[]{"2024-01-01", "2025-01-01"}; + final Object[] expectedOutputParams = new Object[]{ + new ParamsWithTypeConversion( + LocalDate.of(2024, Month.JANUARY, 1).atStartOfDay(ZoneOffset.UTC), + LocalDate.of(2025, Month.JANUARY, 1).atStartOfDay(ZoneOffset.UTC) + .plusDays(1) + .minusSeconds(1))}; + + final Object[] actualOutputParams = buildMethodParams(method, REQUEST_DETAILS, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + private Object[] buildMethodParams(Method theMethod, RequestDetails theRequestDetails, Object[] theInputParams) { + return new BaseMethodBindingMethodParameterBuilder(theMethod, theRequestDetails, theInputParams) + .build(); + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 24646e17f5af..3645702dbe3b 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.method; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; @@ -19,6 +20,7 @@ import org.hl7.fhir.r4.model.StringType; import java.lang.reflect.Method; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -42,6 +44,8 @@ class InnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE = "sampleMethodEmbeddedTypeNoRequestDetailsWithIdType"; static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; + static final String SIMPLE_METHOD_WITH_PARAMS_CONVERSION = "simpleMethodWithParamsConversion"; + static final String EXPAND = "expand"; static final String OP_INSTANCE_OR_TYPE = "opInstanceOrType"; @@ -190,6 +194,48 @@ public String toString() { } } + static class ParamsWithTypeConversion { + @OperationEmbeddedParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) + private final ZonedDateTime myPeriodStart; + + @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + private final ZonedDateTime myPeriodEnd; + + public ParamsWithTypeConversion(ZonedDateTime myPeriodStart, ZonedDateTime myPeriodEnd) { + this.myPeriodStart = myPeriodStart; + this.myPeriodEnd = myPeriodEnd; + } + + public ZonedDateTime getPeriodStart() { + return myPeriodStart; + } + + public ZonedDateTime getPeriodEnd() { + return myPeriodEnd; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ParamsWithTypeConversion that = (ParamsWithTypeConversion) o; + return Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd); + } + + @Override + public int hashCode() { + return Objects.hash(myPeriodStart, myPeriodEnd); + } + + @Override + public String toString() { + return new StringJoiner(", ", ParamsWithTypeConversion.class.getSimpleName() + "[", "]") + .add("myPeriodStart=" + myPeriodStart) + .add("myPeriodEnd=" + myPeriodEnd) + .toString(); + } + } + // Ignore warnings that these classes can be records. Converting them to records will make the tests fail static class SampleParamsWithIdParam { @IdParam @@ -271,6 +317,12 @@ String sampleMethodEmbeddedTypeNoRequestDetails(SampleParams theParams) { return theParams.getParam1(); } + @Operation(name="simpleMethodWithParamsConversion") + String simpleMethodWithParamsConversion(ParamsWithTypeConversion theParams) { + // return something arbitrary + return theParams.getPeriodStart().toString(); + } + String sampleMethodParamNoEmbeddedType(ParamsWithoutAnnotations theParams) { // return something arbitrary return theParams.getParam1(); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index d4e1fc01dd00..9d633dddb052 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -3,17 +3,16 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithTypeConversion; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -24,6 +23,7 @@ import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -108,8 +108,8 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); final List expectedParameters = List.of( - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null) + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class) ); assertThat(resourceParameters) @@ -127,8 +127,8 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null) + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class) ); assertThat(resourceParameters) @@ -145,8 +145,8 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), new RequestDetailsParameterToAssert() ); @@ -165,9 +165,27 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean") + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); + } + + @Test + void paramsConversionZonedDateTime() { + final List resourceParameters = getMethodAndExecute(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + + final List expectedParameters = List.of( + new OperationEmbeddedParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class), + new OperationEmbeddedParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class) ); assertThat(resourceParameters) @@ -371,6 +389,7 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I assertThat(actualOperationEmbeddedParameter.getName()).isEqualTo(expectedOperationEmbeddedParameter.myName()); assertThat(actualOperationEmbeddedParameter.getParamType()).isEqualTo(expectedOperationEmbeddedParameter.myParamType()); assertThat(actualOperationEmbeddedParameter.getInnerCollectionType()).isEqualTo(expectedOperationEmbeddedParameter.myInnerCollectionType()); + assertThat(actualOperationEmbeddedParameter.getSourceType()).isEqualTo(expectedOperationEmbeddedParameter.myTypeToConvertFrom()); return true; } @@ -403,6 +422,7 @@ private record OperationEmbeddedParameterToAssert( @SuppressWarnings("rawtypes") Class myInnerCollectionType, Class myParameterType, - String myParamType) implements IParameterToAssert { + String myParamType, + Class myTypeToConvertFrom) implements IParameterToAssert { } } From 35a5318795fac9f38c2815db5c161eec8b50a259 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 11:30:57 -0500 Subject: [PATCH 48/75] Copyright headers. Address TODOs. Convert the other params classes to use ZonedDateTime. Some new testing and tweaks. javadoc. --- .../uhn/fhir/parser/PreserveStringReader.java | 19 ++ .../EmbeddedParameterRangeType.java | 24 +- .../annotation/OperationEmbeddedParam.java | 13 +- .../ca/uhn/fhir/util/ReflectionUtilTest.java | 39 ++- .../FhirContextValidationSupportSvc.java | 19 ++ ...seMethodBindingMethodParameterBuilder.java | 19 ++ .../server/method/EmbeddedOperationUtils.java | 48 ++- .../method/EmbeddedParameterConverter.java | 19 ++ .../EmbeddedParameterConverterContext.java | 19 ++ .../fhir/rest/server/method/MethodUtil.java | 17 +- .../method/OperationEmbeddedParameter.java | 2 +- .../method/OperationIdParamDetails.java | 19 ++ .../method/ParamInitializationContext.java | 19 ++ .../method/StringTimePeriodHandler.java | 2 +- .../MigrationTaskExecutionResultEnum.java | 19 ++ .../r4/measure/CareGapsOperationProvider.java | 5 +- .../fhir/cr/r4/measure/CareGapsParams.java | 89 +++--- .../measure/EvaluateMeasureSingleParams.java | 19 ++ .../measure/EvaluateMeasureSingleParams2.java | 279 ------------------ 19 files changed, 352 insertions(+), 337 deletions(-) delete mode 100644 hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java index b10d4350f3cc..aba207c62673 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java @@ -1,3 +1,22 @@ +/*- + * #%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.parser; import jakarta.annotation.Nonnull; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java index d54c132885c0..ac9249f0dd25 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java @@ -1,6 +1,28 @@ +/*- + * #%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; -// LUKETODO: javadoc +/** + * Used to indicate whether an {@link OperationEmbeddedParam} 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 EmbeddedParameterRangeType { START, END, diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java index 632866ebc2c3..652e7ae82726 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java @@ -80,11 +80,18 @@ */ Class type() default IBase.class; - // LUKETODO: javadoc - // LUKETODO: Void to mean don't convert? + /** + * 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; - // LUKETODO: javadoc + /** + * @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. + */ EmbeddedParameterRangeType rangeType() default EmbeddedParameterRangeType.NOT_APPLICABLE; /** diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java index 913ad9514c35..9334351a54f0 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ReflectionUtilTest.java @@ -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; @@ -17,8 +19,6 @@ public class ReflectionUtilTest { - // LUKETODO: add tests for new methods - @Test public void testNewInstance() { assertEquals(ArrayList.class, ReflectionUtil.newInstance(ArrayList.class).getClass()); @@ -67,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> result = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation(method, TestAnnotation.class); + + assertEquals(2, result.size()); + assertTrue(result.contains(TestClass1.class)); + assertTrue(result.contains(TestClass3.class)); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java index eb044eff6c6d..3cafb9bfad2d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * 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.jpa.validation; import ca.uhn.fhir.context.FhirContext; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 9766c5cf8f7c..b07d2e0a3d2d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import ca.uhn.fhir.context.ConfigurationException; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 348bbbc94019..1b3cbed47893 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -1,8 +1,28 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -12,14 +32,23 @@ import java.time.ZonedDateTime; import java.util.Collection; -// LUKETODO: javadoc -// LUKETODO: merge with ReflectionUtil or ParameterUtil? -// LUKETODO: think about Exceptions +/** + * Common operations for any functionality that work with {@link OperationEmbeddedParam} + */ public class EmbeddedOperationUtils { private EmbeddedOperationUtils() {} - // LUKETODO: javadoc + /** + * Validate that a constructor for a class with fields that are {@link OperationEmbeddedParam} declares its + * parameters in the same order as the fields are declared in the class. It also validates that the fields are + * final. It also takes into account Collections and generic types, as well as whether there is a source to + * target type conversion, such as String to ZonedDateTime. + * + * @param theParameterTypeWithOperationEmbeddedParam the class that has fields that are + * annotated with {@link OperationEmbeddedParam} + * @return the constructor for the class + */ static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { final Constructor[] constructors = theParameterTypeWithOperationEmbeddedParam.getConstructors(); @@ -43,8 +72,15 @@ static Constructor validateAndGetConstructor(Class theParameterTypeWithOpe return soleConstructor; } - // LUKETODO: javadoc - // We currently only support converting from a String to a ZonedDateTime + /** + * Indicate whether or not this is currently a supported type conversion + * We currently only support converting from a String to a ZonedDateTime + * + * @param theSourceType The source type for the class, which can be different from the declared type + * @param theTargetType The target type for the class, which can be different from the source type + * @param theEmbeddedParameterRangeType Whether the embedded parameter is a range and if so, start or end + * @return true if the type conversion is supported + */ static boolean isValidSourceTypeConversion( Class theSourceType, Class theTargetType, EmbeddedParameterRangeType theEmbeddedParameterRangeType) { return String.class == theSourceType diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 019a259ed812..33f39beeebd7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import ca.uhn.fhir.context.ConfigurationException; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java index f459a1c6cee1..aac52f4b2e93 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import jakarta.annotation.Nullable; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 45ccd3f13fec..68d0ba72e67f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -74,6 +74,7 @@ import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Optional; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -140,13 +141,13 @@ public static List getResourceParameters( declaredParameterType = parameterType; } - if (Collection.class.isAssignableFrom(parameterType)) { + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); declaredParameterType = parameterType; } - if (Collection.class.isAssignableFrom(parameterType)) { + if (parameterType == null || Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() + "' in type '" @@ -167,10 +168,14 @@ public static List getResourceParameters( ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); if (Date.class.equals(genericType)) { BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); - parameterType = dateTimeDef.getImplementingClass(); + parameterType = Optional.ofNullable(dateTimeDef) + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); } else if (String.class.equals(genericType) || genericType == null) { BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); - parameterType = dateTimeDef.getImplementingClass(); + parameterType = Optional.ofNullable(dateTimeDef) + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); } } } @@ -456,9 +461,7 @@ public Object outgoingClient(Object theObject) { } } - if (paramContexts.isEmpty() - || !(param - instanceof OperationEmbeddedParameter)) { // LUKETODO: another nasty hack: we need to add + if (paramContexts.isEmpty() || !(param instanceof OperationEmbeddedParameter)) { // RequestDetails if it's last paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index c37764c13e60..ebb697206829 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -109,7 +109,7 @@ public class OperationEmbeddedParameter implements IParameter { myContext = theCtx; myDescription = theDescription; // LUKETODO: is this wise? - if (theSourceType == Void.class) { + if (theSourceType == Void.class && myParameterType != null) { mySourceType = myParameterType; } else { mySourceType = theSourceType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java index 011d99464284..65c7a90e08e6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationIdParamDetails.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import ca.uhn.fhir.rest.annotation.IdParam; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java index bdee8e6b4f67..0b4a1e91957d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; import java.lang.reflect.Method; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java index df6651bf701a..379bb2ac4e16 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/StringTimePeriodHandler.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR - Clinical Reasoning + * HAPI FHIR - Server Framework * %% * Copyright (C) 2014 - 2025 Smile CDR, Inc. * %% diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java index 6aaf8cf05a4b..5c36fd28ae78 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Server - SQL Migration + * %% + * 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.jpa.migrate.taskdef; public enum MigrationTaskExecutionResultEnum { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index a28f4115400f..8b739490e8d4 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -87,9 +87,8 @@ public Parameters careGapsReport(RequestDetails theRequestDetails, CareGapsParam return myR4CareGapsProcessorFactory .create(theRequestDetails) .getCareGapsReport( - // LUKETODO: how to handle passing this down seamlessly? - myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), - myStringTimePeriodHandler.getEndZonedDateTime(theParams.getPeriodEnd(), theRequestDetails), + theParams.getPeriodStart(), + theParams.getPeriodEnd(), theParams.getSubject(), theParams.getStatus(), theParams.getMeasureId() == null diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 851197be6530..e5c622109c9b 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -1,9 +1,30 @@ +/*- + * #%L + * HAPI FHIR - Clinical Reasoning + * %% + * 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.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; +import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.StringJoiner; @@ -50,11 +71,11 @@ * If 'true', this will return summarized subject bundle with only detectedIssue resource. */ public class CareGapsParams { - @OperationEmbeddedParam(name = "periodStart") - private final String myPeriodStart; + @OperationEmbeddedParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) + private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd") - private final String myPeriodEnd; + @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + private final ZonedDateTime myPeriodEnd; @OperationEmbeddedParam(name = "subject") private final String mySubject; @@ -75,8 +96,8 @@ public class CareGapsParams { private final BooleanType myNonDocument; public CareGapsParams( - String thePeriodStart, - String thePeriodEnd, + ZonedDateTime thePeriodStart, + ZonedDateTime thePeriodEnd, String theSubject, List theStatus, List theMeasureId, @@ -94,21 +115,21 @@ public CareGapsParams( } private CareGapsParams(Builder builder) { - this.myPeriodStart = builder.myPeriodStart; - this.myPeriodEnd = builder.myPeriodEnd; - this.mySubject = builder.mySubject; - this.myStatus = builder.myStatus; - this.myMeasureId = builder.myMeasureId; - this.myMeasureIdentifier = builder.myMeasureIdentifier; - this.myMeasureUrl = builder.myMeasureUrl; - this.myNonDocument = builder.myNonDocument; + myPeriodStart = builder.myPeriodStart; + myPeriodEnd = builder.myPeriodEnd; + mySubject = builder.mySubject; + myStatus = builder.myStatus; + myMeasureId = builder.myMeasureId; + myMeasureIdentifier = builder.myMeasureIdentifier; + myMeasureUrl = builder.myMeasureUrl; + myNonDocument = builder.myNonDocument; } - public String getPeriodStart() { + public ZonedDateTime getPeriodStart() { return myPeriodStart; } - public String getPeriodEnd() { + public ZonedDateTime getPeriodEnd() { return myPeriodEnd; } @@ -182,8 +203,8 @@ public static Builder builder() { } public static class Builder { - private String myPeriodStart; - private String myPeriodEnd; + private ZonedDateTime myPeriodStart; + private ZonedDateTime myPeriodEnd; private String mySubject; private List myStatus; private List myMeasureId; @@ -191,43 +212,43 @@ public static class Builder { private List myMeasureUrl; private BooleanType myNonDocument; - public Builder setPeriodStart(String myPeriodStart) { - this.myPeriodStart = myPeriodStart; + public Builder setPeriodStart(ZonedDateTime thePeriodStart) { + myPeriodStart = thePeriodStart; return this; } - public Builder setPeriodEnd(String myPeriodEnd) { - this.myPeriodEnd = myPeriodEnd; + public Builder setPeriodEnd(ZonedDateTime thePeriodEnd) { + myPeriodEnd = thePeriodEnd; return this; } - public Builder setSubject(String mySubject) { - this.mySubject = mySubject; + public Builder setSubject(String theSubject) { + mySubject = theSubject; return this; } - public Builder setStatus(List myStatus) { - this.myStatus = myStatus; + public Builder setStatus(List theStatus) { + myStatus = theStatus; return this; } - public Builder setMeasureId(List myMeasureId) { - this.myMeasureId = myMeasureId; + public Builder setMeasureId(List theMeasureId) { + myMeasureId = theMeasureId; return this; } - public Builder setMeasureIdentifier(List myMeasureIdentifier) { - this.myMeasureIdentifier = myMeasureIdentifier; + public Builder setMeasureIdentifier(List theMeasureIdentifier) { + myMeasureIdentifier = theMeasureIdentifier; return this; } - public Builder setMeasureUrl(List myMeasureUrl) { - this.myMeasureUrl = myMeasureUrl; + public Builder setMeasureUrl(List theMeasureUrl) { + myMeasureUrl = theMeasureUrl; return this; } - public Builder setNonDocument(BooleanType myNonDocument) { - this.myNonDocument = myNonDocument; + public Builder setNonDocument(BooleanType theNonDocument) { + myNonDocument = theNonDocument; return this; } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 80e4034a9fda..536af0e9d803 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Clinical Reasoning + * %% + * 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.cr.r4.measure; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java deleted file mode 100644 index 3012af0ab011..000000000000 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams2.java +++ /dev/null @@ -1,279 +0,0 @@ -package ca.uhn.fhir.cr.r4.measure; - -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Endpoint; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; - -import java.time.ZonedDateTime; -import java.util.Objects; -import java.util.StringJoiner; - -/** - * Non-RequestDetails parameters for the $evaluate-measure - * operation found in the - * FHIR Clinical - * Reasoning Module. This implementation aims to be compatible with the CQF - * IG. - *

- * myeId the id of the Measure to evaluate - * myPeriodStart The start of the reporting period - * myPeriodEnd The end of the reporting period - * myReportType The type of MeasureReport to generate - * mySubject the subject to use for the evaluation - * myPractitioner the practitioner to use for the evaluation - * myLastReceivedOn the date the results of this measure were last - * received. - * myProductLine the productLine (e.g. Medicare, Medicaid, etc) to use - * for the evaluation. This is a non-standard parameter. - * myAdditionalData the data bundle containing additional data - */ -public class EvaluateMeasureSingleParams2 { - @IdParam - private final IdType myId; - - @OperationEmbeddedParam(name = "periodStart", sourceType = String.class) - private final ZonedDateTime myPeriodStart; - - @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class) - private final ZonedDateTime myPeriodEnd; - - @OperationEmbeddedParam(name = "reportType") - private final String myReportType; - - @OperationEmbeddedParam(name = "subject") - private final String mySubject; - - @OperationEmbeddedParam(name = "practitioner") - private final String myPractitioner; - - @OperationEmbeddedParam(name = "lastReceivedOn") - private final String myLastReceivedOn; - - @OperationEmbeddedParam(name = "productLine") - private final String myProductLine; - - @OperationEmbeddedParam(name = "additionalData") - private final Bundle myAdditionalData; - - @OperationEmbeddedParam(name = "terminologyEndpoint") - private final Endpoint myTerminologyEndpoint; - - @OperationEmbeddedParam(name = "parameters") - private final Parameters myParameters; - - public EvaluateMeasureSingleParams2( - IdType theId, - ZonedDateTime thePeriodStart, - ZonedDateTime thePeriodEnd, - String theReportType, - String theSubject, - String thePractitioner, - String theLastReceivedOn, - String theProductLine, - Bundle theAdditionalData, - Endpoint theTerminologyEndpoint, - Parameters theParameters) { - myId = theId; - myPeriodStart = thePeriodStart; - myPeriodEnd = thePeriodEnd; - myReportType = theReportType; - mySubject = theSubject; - myPractitioner = thePractitioner; - myLastReceivedOn = theLastReceivedOn; - myProductLine = theProductLine; - myAdditionalData = theAdditionalData; - myTerminologyEndpoint = theTerminologyEndpoint; - myParameters = theParameters; - } - - private EvaluateMeasureSingleParams2(Builder builder) { - this.myId = builder.myId; - this.myPeriodStart = builder.myPeriodStart; - this.myPeriodEnd = builder.myPeriodEnd; - this.myReportType = builder.myReportType; - this.mySubject = builder.mySubject; - this.myPractitioner = builder.myPractitioner; - this.myLastReceivedOn = builder.myLastReceivedOn; - this.myProductLine = builder.myProductLine; - this.myAdditionalData = builder.myAdditionalData; - this.myTerminologyEndpoint = builder.myTerminologyEndpoint; - this.myParameters = builder.myParameters; - } - - public IdType getId() { - return myId; - } - - public ZonedDateTime getPeriodStart() { - return myPeriodStart; - } - - public ZonedDateTime getPeriodEnd() { - return myPeriodEnd; - } - - public String getReportType() { - return myReportType; - } - - public String getSubject() { - return mySubject; - } - - public String getPractitioner() { - return myPractitioner; - } - - public String getLastReceivedOn() { - return myLastReceivedOn; - } - - public String getProductLine() { - return myProductLine; - } - - public Bundle getAdditionalData() { - return myAdditionalData; - } - - public Endpoint getTerminologyEndpoint() { - return myTerminologyEndpoint; - } - - public Parameters getParameters() { - return myParameters; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - EvaluateMeasureSingleParams2 that = (EvaluateMeasureSingleParams2) o; - return Objects.equals(myId, that.myId) - && Objects.equals(myPeriodStart, that.myPeriodStart) - && Objects.equals(myPeriodEnd, that.myPeriodEnd) - && Objects.equals(myReportType, that.myReportType) - && Objects.equals(mySubject, that.mySubject) - && Objects.equals(myPractitioner, that.myPractitioner) - && Objects.equals(myLastReceivedOn, that.myLastReceivedOn) - && Objects.equals(myProductLine, that.myProductLine) - && Objects.equals(myAdditionalData, that.myAdditionalData) - && Objects.equals(myTerminologyEndpoint, that.myTerminologyEndpoint) - && Objects.equals(myParameters, that.myParameters); - } - - @Override - public int hashCode() { - return Objects.hash( - myId, - myPeriodStart, - myPeriodEnd, - myReportType, - mySubject, - myPractitioner, - myLastReceivedOn, - myProductLine, - myAdditionalData, - myTerminologyEndpoint, - myParameters); - } - - @Override - public String toString() { - return new StringJoiner(", ", EvaluateMeasureSingleParams2.class.getSimpleName() + "[", "]") - .add("myId=" + myId) - .add("myPeriodStart='" + myPeriodStart + "'") - .add("myPeriodEnd='" + myPeriodEnd + "'") - .add("myReportType='" + myReportType + "'") - .add("mySubject='" + mySubject + "'") - .add("myPractitioner='" + myPractitioner + "'") - .add("myLastReceivedOn='" + myLastReceivedOn + "'") - .add("myProductLine='" + myProductLine + "'") - .add("myAdditionalData=" + myAdditionalData) - .add("myTerminologyEndpoint=" + myTerminologyEndpoint) - .add("myParameters=" + myParameters) - .toString(); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private IdType myId; - private ZonedDateTime myPeriodStart; - private ZonedDateTime myPeriodEnd; - private String myReportType; - private String mySubject; - private String myPractitioner; - private String myLastReceivedOn; - private String myProductLine; - private Bundle myAdditionalData; - private Endpoint myTerminologyEndpoint; - private Parameters myParameters; - - public Builder setId(IdType myId) { - this.myId = myId; - return this; - } - - public Builder setPeriodStart(ZonedDateTime myPeriodStart) { - this.myPeriodStart = myPeriodStart; - return this; - } - - public Builder setPeriodEnd(ZonedDateTime myPeriodEnd) { - this.myPeriodEnd = myPeriodEnd; - return this; - } - - public Builder setReportType(String myReportType) { - this.myReportType = myReportType; - return this; - } - - public Builder setSubject(String mySubject) { - this.mySubject = mySubject; - return this; - } - - public Builder setPractitioner(String myPractitioner) { - this.myPractitioner = myPractitioner; - return this; - } - - public Builder setLastReceivedOn(String myLastReceivedOn) { - this.myLastReceivedOn = myLastReceivedOn; - return this; - } - - public Builder setProductLine(String myProductLine) { - this.myProductLine = myProductLine; - return this; - } - - public Builder setAdditionalData(Bundle myAdditionalData) { - this.myAdditionalData = myAdditionalData; - return this; - } - - public Builder setTerminologyEndpoint(Endpoint myTerminologyEndpoint) { - this.myTerminologyEndpoint = myTerminologyEndpoint; - return this; - } - - public Builder setParameters(Parameters myParameters) { - this.myParameters = myParameters; - return this; - } - - public EvaluateMeasureSingleParams2 build() { - return new EvaluateMeasureSingleParams2(this); - } - } -} From 73b5a1f815af29b636adc8caf0fc1d0deb3119a7 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 11:33:44 -0500 Subject: [PATCH 49/75] Spotless. --- .../fhir/rest/server/method/EmbeddedOperationUtils.java | 2 +- .../java/ca/uhn/fhir/rest/server/method/MethodUtil.java | 8 ++++---- .../java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 1b3cbed47893..299dd39715f1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -46,7 +46,7 @@ private EmbeddedOperationUtils() {} * target type conversion, such as String to ZonedDateTime. * * @param theParameterTypeWithOperationEmbeddedParam the class that has fields that are - * annotated with {@link OperationEmbeddedParam} + * annotated with {@link OperationEmbeddedParam} * @return the constructor for the class */ static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 68d0ba72e67f..dffddae01b06 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -169,13 +169,13 @@ public static List getResourceParameters( if (Date.class.equals(genericType)) { BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); parameterType = Optional.ofNullable(dateTimeDef) - .map(BaseRuntimeElementDefinition::getImplementingClass) - .orElse(null); + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); } else if (String.class.equals(genericType) || genericType == null) { BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); parameterType = Optional.ofNullable(dateTimeDef) - .map(BaseRuntimeElementDefinition::getImplementingClass) - .orElse(null); + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); } } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index e5c622109c9b..c987e5c1a78a 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -71,7 +71,10 @@ * If 'true', this will return summarized subject bundle with only detectedIssue resource. */ public class CareGapsParams { - @OperationEmbeddedParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) + @OperationEmbeddedParam( + name = "periodStart", + sourceType = String.class, + rangeType = EmbeddedParameterRangeType.START) private final ZonedDateTime myPeriodStart; @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) From dc06188bb5fe7471f878912e2cd422c6a25a745c Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 13:28:05 -0500 Subject: [PATCH 50/75] Renames refactors cleanup TODOs. --- ...Param.java => EmbeddedOperationParam.java} | 6 +- .../EmbeddedParameterRangeType.java | 2 +- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 11 +++- ...seMethodBindingMethodParameterBuilder.java | 34 ++++------ ...r.java => EmbeddedOperationParameter.java} | 16 ++--- .../server/method/EmbeddedOperationUtils.java | 64 +++++++++++++++++-- .../method/EmbeddedParameterConverter.java | 22 +++---- .../fhir/rest/server/method/MethodUtil.java | 6 +- .../server/method/OperationMethodBinding.java | 30 +++++++-- .../ca/uhn/fhir/cr/config/r4/CrR4Config.java | 11 ++-- .../r4/measure/CareGapsOperationProvider.java | 7 +- .../fhir/cr/r4/measure/CareGapsParams.java | 18 +++--- .../measure/EvaluateMeasureSingleParams.java | 23 +++---- .../r4/measure/MeasureOperationsProvider.java | 7 +- ...thodBindingMethodParameterBuilderTest.java | 11 ++++ .../server/method/InnerClassesAndMethods.java | 16 ++--- .../rest/server/method/MethodUtilTest.java | 46 ++++++------- 17 files changed, 201 insertions(+), 129 deletions(-) rename hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/{OperationEmbeddedParam.java => EmbeddedOperationParam.java} (96%) rename hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/{OperationEmbeddedParameter.java => EmbeddedOperationParameter.java} (97%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java similarity index 96% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java index 652e7ae82726..1c79661d97f5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationEmbeddedParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java @@ -38,16 +38,16 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.PARAMETER, ElementType.FIELD}) -public @interface OperationEmbeddedParam { +public @interface EmbeddedOperationParam { // LUKETODO: after writing the conformance code get rid of a bunch of cruft like MAX_UNLIMITED /** - * Value for {@link OperationEmbeddedParam#max()} indicating no maximum + * Value for {@link EmbeddedOperationParam#max()} indicating no maximum */ int MAX_UNLIMITED = -1; /** - * Value for {@link OperationEmbeddedParam#max()} indicating that the maximum will be inferred + * Value for {@link EmbeddedOperationParam#max()} indicating that the maximum will be inferred * from the type. If the type is a single parameter type (e.g. StringDt, * TokenParam, IBaseResource) the maximum will be * 1. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java index ac9249f0dd25..c0298bdb5c2f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.annotation; /** - * Used to indicate whether an {@link OperationEmbeddedParam} should be considered as part of a range of values, and if + * Used to indicate whether an {@link EmbeddedOperationParam} 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 EmbeddedParameterRangeType { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index 4afdd593a2a5..b6d5a8b72d3c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -296,11 +296,18 @@ public static boolean typeExists(String theName) { public static List> getMethodParamsWithClassesWithFieldsWithAnnotation( Method theMethod, Class theAnnotationClass) { return Arrays.stream(theMethod.getParameterTypes()) - .filter(paramType -> hasFieldsWithAnnotation(paramType, theAnnotationClass)) + .filter(paramType -> hasAnyFieldsWithAnnotation(paramType, theAnnotationClass)) .collect(Collectors.toList()); } - private static boolean hasFieldsWithAnnotation(Class paramType, Class theAnnotationClass) { + public static boolean hasAnyMethodParamsWithClassesWithFieldsWithAnnotation( + Method theMethod, Class theAnnotationClass) { + return Arrays.stream(theMethod.getParameterTypes()) + .anyMatch(paramType -> hasAnyFieldsWithAnnotation(paramType, theAnnotationClass)); + } + + private static boolean hasAnyFieldsWithAnnotation( + Class paramType, Class theAnnotationClass) { return Arrays.stream(paramType.getDeclaredFields()) .anyMatch(field -> field.isAnnotationPresent(theAnnotationClass)); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index b07d2e0a3d2d..c0c9d79fa3da 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -39,7 +39,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -49,10 +48,9 @@ import static java.util.function.Predicate.not; -// LUKETODO: should this be responsible for invoking the method as well? /** * Responsible for either passing to objects params straight through to the method call or converting them to - * fit within a class that has fields annotated with {@link OperationEmbeddedParam} and to also handle placement + * fit within a class that has fields annotated with {@link EmbeddedOperationParam} and to also handle placement * of {@link RequestDetails} in those params */ class BaseMethodBindingMethodParameterBuilder { @@ -103,7 +101,7 @@ private Object[] tryBuildMethodParams() final List> parameterTypesWithOperationEmbeddedParam = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - myMethod, OperationEmbeddedParam.class); + myMethod, EmbeddedOperationParam.class); if (parameterTypesWithOperationEmbeddedParam.size() > 1) { throw new InternalErrorException(String.format( @@ -216,16 +214,9 @@ private Object[] convertParamsIfNeeded( Object[] theMethodParamsWithoutRequestDetails, Parameter[] theConstructorParameters, Annotation[] theAnnotations) { - // LUKETODO: rangetype check? - // LUKETODO: warnings? - if (Arrays.stream(theMethodParamsWithoutRequestDetails).noneMatch(String.class::isInstance) - && Arrays.stream(theAnnotations) - .filter(OperationEmbeddedParam.class::isInstance) - .map(OperationEmbeddedParam.class::cast) - .map(OperationEmbeddedParam::sourceType) - .noneMatch(ZonedDateTime.class::isInstance)) { - - // Nothing to do: + + if (!EmbeddedOperationUtils.hasAnyValidSourceTypeConversions( + theMethodParamsWithoutRequestDetails, theConstructorParameters, theAnnotations)) { return theMethodParamsWithoutRequestDetails; } @@ -249,11 +240,11 @@ private Object convertParamIfNeeded( return paramAtIndex; } - if (!(annotation instanceof OperationEmbeddedParam)) { + if (!(annotation instanceof EmbeddedOperationParam)) { return paramAtIndex; } - final OperationEmbeddedParam embeddedParamAtIndex = (OperationEmbeddedParam) annotation; + final EmbeddedOperationParam embeddedParamAtIndex = (EmbeddedOperationParam) annotation; final Class paramClassAtIndex = paramAtIndex.getClass(); final EmbeddedParameterRangeType rangeType = embeddedParamAtIndex.rangeType(); final Parameter constructorParameter = theConstructorParameters[theIndex]; @@ -355,12 +346,9 @@ private void validateMethodParamType(Object theMethodParam, Class theParamete final Class methodParamClass = theMethodParam.getClass(); - // LUKETODO: HAPI-4313421: Mismatch between methodParamClass: class org.hl7.fhir.r4.model.IdType and - // OperationEmbeddedParam source type: class java.lang.String for method: evaluateMeasure - - final Optional optOperationEmbeddedParam = - theAnnotation instanceof OperationEmbeddedParam - ? Optional.of((OperationEmbeddedParam) theAnnotation) + final Optional optOperationEmbeddedParam = + theAnnotation instanceof EmbeddedOperationParam + ? Optional.of((EmbeddedOperationParam) theAnnotation) : Optional.empty(); optOperationEmbeddedParam.ifPresent(embeddedParam -> { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java similarity index 97% rename from hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java index ebb697206829..2bef7e73fdbc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java @@ -26,9 +26,9 @@ import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -59,15 +59,15 @@ // LUKETODO: consider deleting whatever code may be unused /** - * Associated with a field annotated with {@link OperationEmbeddedParam} within a class passed to a method annotated with + * Associated with a field annotated with {@link EmbeddedOperationParam} within a class passed to a method annotated with * {@link Operation}. */ -public class OperationEmbeddedParameter implements IParameter { - private static final Logger ourLog = LoggerFactory.getLogger(OperationEmbeddedParameter.class); +public class EmbeddedOperationParameter implements IParameter { + private static final Logger ourLog = LoggerFactory.getLogger(EmbeddedOperationParameter.class); // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? // LUKETODO: if so, add conditional logic everywhere to use it - // static final String REQUEST_CONTENTS_USERDATA_KEY = OperationEmbeddedParam.class.getName() + "_PARSED_RESOURCE"; + static final String REQUEST_CONTENTS_USERDATA_KEY = EmbeddedOperationParameter.class.getName() + "_PARSED_RESOURCE"; @SuppressWarnings("unchecked") private static final Class[] COMPOSITE_TYPES = new Class[0]; @@ -92,7 +92,7 @@ public class OperationEmbeddedParameter implements IParameter { // LUKETODO: just pass the whole thing? private final EmbeddedParameterRangeType myRengeType; - OperationEmbeddedParameter( + EmbeddedOperationParameter( FhirContext theCtx, String theOperationName, String theParameterName, @@ -290,7 +290,7 @@ public static void validateTypeIsAppropriateVersionForContext( } } - OperationEmbeddedParameter setConverter(IOperationParamConverter theConverter) { + EmbeddedOperationParameter setConverter(IOperationParamConverter theConverter) { myConverter = theConverter; return this; } @@ -435,7 +435,7 @@ private void translateQueryParametersIntoServerArgumentForGet( HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer(); String msg = localizer.getMessage( - OperationEmbeddedParameter.class, "urlParamNotPrimitive", myOperationName, myName); + EmbeddedOperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); // LUKETODO: claim new code throw new MethodNotAllowedException(Msg.code(99999993) + msg, RequestTypeEnum.POST); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 299dd39715f1..2bf3919804a9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -21,32 +21,38 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +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.time.ZonedDateTime; import java.util.Collection; +import java.util.stream.IntStream; /** - * Common operations for any functionality that work with {@link OperationEmbeddedParam} + * Common operations for any functionality that work with {@link EmbeddedOperationParam} */ public class EmbeddedOperationUtils { private EmbeddedOperationUtils() {} /** - * Validate that a constructor for a class with fields that are {@link OperationEmbeddedParam} declares its + * Validate that a constructor for a class with fields that are {@link EmbeddedOperationParam} declares its * parameters in the same order as the fields are declared in the class. It also validates that the fields are * final. It also takes into account Collections and generic types, as well as whether there is a source to * target type conversion, such as String to ZonedDateTime. * * @param theParameterTypeWithOperationEmbeddedParam the class that has fields that are - * annotated with {@link OperationEmbeddedParam} + * annotated with {@link EmbeddedOperationParam} * @return the constructor for the class */ static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { @@ -72,6 +78,56 @@ static Constructor validateAndGetConstructor(Class theParameterTypeWithOpe return soleConstructor; } + static boolean hasAnyMethodParamsWithClassesWithFieldsWithEmbeddedOperationParams(Method theMethod) { + return ReflectionUtil.hasAnyMethodParamsWithClassesWithFieldsWithAnnotation( + theMethod, EmbeddedOperationParam.class); + } + + // LUKETODO: javadoc + static boolean hasAnyValidSourceTypeConversions( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations) { + if (theMethodParamsWithoutRequestDetails.length != theAnnotations.length + || theMethodParamsWithoutRequestDetails.length != theConstructorParameters.length) { + // This is probably an error but not this method's responsibility to check + return false; + } + + return IntStream.range(0, theMethodParamsWithoutRequestDetails.length) + .mapToObj(index -> isValidSourceTypeConversion( + theMethodParamsWithoutRequestDetails, theConstructorParameters, theAnnotations, index)) + .anyMatch(Boolean::booleanValue); + } + + @Nonnull + private static Boolean isValidSourceTypeConversion( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations, + int theIndex) { + final Object methodParam = theMethodParamsWithoutRequestDetails[theIndex]; + + if (methodParam == null) { + return false; + } + + final Class methodParamClass = methodParam.getClass(); + final Class constructorParamType = theConstructorParameters[theIndex].getType(); + final Annotation annotation = theAnnotations[theIndex]; + + if (annotation instanceof EmbeddedOperationParam) { + final EmbeddedOperationParam embeddedOperationParam = (EmbeddedOperationParam) annotation; + final EmbeddedParameterRangeType embeddedParameterRangeType = embeddedOperationParam.rangeType(); + + if (isValidSourceTypeConversion(methodParamClass, constructorParamType, embeddedParameterRangeType)) { + return true; + } + } + + return false; + } + /** * Indicate whether or not this is currently a supported type conversion * We currently only support converting from a String to a ZonedDateTime diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 33f39beeebd7..d867ee9eafdb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -22,9 +22,9 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; @@ -40,8 +40,8 @@ import static org.slf4j.LoggerFactory.*; /** - * Leveraged by {@link MethodUtil} exclusively to convert {@link OperationEmbeddedParam} parameters for a method to - * either a {@link NullParameter} or an {@link OperationEmbeddedParam}. + * Leveraged by {@link MethodUtil} exclusively to convert {@link EmbeddedOperationParam} parameters for a method to + * either a {@link NullParameter} or an {@link EmbeddedOperationParam}. */ public class EmbeddedParameterConverter { private static final org.slf4j.Logger ourLog = getLogger(EmbeddedParameterConverter.class); @@ -91,9 +91,9 @@ private EmbeddedParameterConverterContext convertField(Field theField) { if (fieldAnnotation instanceof IdParam) { return EmbeddedParameterConverterContext.forParameter(new NullParameter()); - } else if (fieldAnnotation instanceof OperationEmbeddedParam) { + } else if (fieldAnnotation instanceof EmbeddedOperationParam) { final ParamInitializationContext paramContext = - buildParamContext(fieldType, theField, (OperationEmbeddedParam) fieldAnnotation); + buildParamContext(fieldType, theField, (EmbeddedOperationParam) fieldAnnotation); return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); } else { @@ -106,10 +106,10 @@ private EmbeddedParameterConverterContext convertField(Field theField) { } private ParamInitializationContext buildParamContext( - Class theFieldType, Field theField, OperationEmbeddedParam theOperationEmbeddedParam) { + Class theFieldType, Field theField, EmbeddedOperationParam theEmbeddedOperationParam) { - final OperationEmbeddedParameter operationEmbeddedParameter = - getOperationEmbeddedParameter(theOperationEmbeddedParam); + final EmbeddedOperationParameter embeddedOperationParameter = + getOperationEmbeddedParameter(theEmbeddedOperationParam); Class parameterType = theFieldType; Class> outerCollectionType = null; @@ -151,14 +151,14 @@ private ParamInitializationContext buildParamContext( // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later return new ParamInitializationContext( - operationEmbeddedParameter, parameterType, outerCollectionType, innerCollectionType); + embeddedOperationParameter, parameterType, outerCollectionType, innerCollectionType); } @Nonnull - private OperationEmbeddedParameter getOperationEmbeddedParameter(OperationEmbeddedParam operationParam) { + private EmbeddedOperationParameter getOperationEmbeddedParameter(EmbeddedOperationParam operationParam) { final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; - return new OperationEmbeddedParameter( + return new EmbeddedOperationParameter( myContext, myOperation.name(), operationParam.name(), diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index dffddae01b06..4299fe311f7f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -31,13 +31,13 @@ import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Elements; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Patch; @@ -204,7 +204,7 @@ public static List getResourceParameters( if (nextParameterAnnotations.length == 0) { final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, OperationEmbeddedParam.class); + methodToUse, EmbeddedOperationParam.class); if (op == null) { throw new ConfigurationException(Msg.code(846192641) @@ -461,7 +461,7 @@ public Object outgoingClient(Object theObject) { } } - if (paramContexts.isEmpty() || !(param instanceof OperationEmbeddedParameter)) { + if (paramContexts.isEmpty() || !(param instanceof EmbeddedOperationParameter)) { // RequestDetails if it's last paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index fa0aaee8c15a..a7556b48da2c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -24,9 +24,9 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.RequiredParam; @@ -57,6 +57,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -362,7 +363,7 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques throws BaseServerResponseException, IOException { if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) { IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null); - theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents); + theRequest.getUserData().put(determineUserDataKey(getMethod()), requestContents); } return super.invokeServer(theServer, theRequest); } @@ -438,8 +439,7 @@ public boolean isDeleteEnabled() { @Override protected void populateRequestDetailsForInterceptor(RequestDetails theRequestDetails, Object[] theMethodParams) { super.populateRequestDetailsForInterceptor(theRequestDetails, theMethodParams); - IBaseResource resource = - (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); + IBaseResource resource = getBaseResource(theRequestDetails); theRequestDetails.setResource(resource); } @@ -453,7 +453,7 @@ public String getCanonicalUrl() { private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, OperationEmbeddedParam.class); + theMethod, EmbeddedOperationParam.class); if (!operationEmbeddedTypes.isEmpty()) { return findIdParamIndexForTypeWithEmbeddedParams(theMethod, operationEmbeddedTypes, theContext); @@ -532,6 +532,26 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( return OperationIdParamDetails.EMPTY; } + private IBaseResource getBaseResource(RequestDetails theRequestDetails) { + final Map userData = theRequestDetails.getUserData(); + + if (userData.containsKey(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY)) { + return (IBaseResource) userData.get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); + } + + if (userData.containsKey(EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY)) { + return (IBaseResource) userData.get(EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY); + } + + return null; + } + + private static String determineUserDataKey(Method theMethod) { + return EmbeddedOperationUtils.hasAnyMethodParamsWithClassesWithFieldsWithEmbeddedOperationParams(theMethod) + ? EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY + : OperationParameter.REQUEST_CONTENTS_USERDATA_KEY; + } + public static class ReturnType { private int myMax; private int myMin; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java index 6a4526dc9b9e..88ddf87d2e14 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/CrR4Config.java @@ -144,9 +144,8 @@ ICareGapsServiceFactory careGapsServiceFactory( } @Bean - CareGapsOperationProvider r4CareGapsOperationProvider( - ICareGapsServiceFactory theR4CareGapsProcessorFactory, StringTimePeriodHandler theStringTimePeriodHandler) { - return new CareGapsOperationProvider(theR4CareGapsProcessorFactory, theStringTimePeriodHandler); + CareGapsOperationProvider r4CareGapsOperationProvider(ICareGapsServiceFactory theR4CareGapsProcessorFactory) { + return new CareGapsOperationProvider(theR4CareGapsProcessorFactory); } @Bean @@ -155,10 +154,8 @@ SubmitDataProvider r4SubmitDataProvider() { } @Bean - MeasureOperationsProvider r4MeasureOperationsProvider( - R4MeasureEvaluatorSingleFactory theR4MeasureServiceFactory, - StringTimePeriodHandler theStringTimePeriodHandler) { - return new MeasureOperationsProvider(theR4MeasureServiceFactory, theStringTimePeriodHandler); + MeasureOperationsProvider r4MeasureOperationsProvider(R4MeasureEvaluatorSingleFactory theR4MeasureServiceFactory) { + return new MeasureOperationsProvider(theR4MeasureServiceFactory); } @Bean diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 8b739490e8d4..f786bbff4a14 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; @@ -39,12 +38,9 @@ public class CareGapsOperationProvider { private static final Logger ourLog = LoggerFactory.getLogger(CareGapsOperationProvider.class); private final ICareGapsServiceFactory myR4CareGapsProcessorFactory; - private final StringTimePeriodHandler myStringTimePeriodHandler; - public CareGapsOperationProvider( - ICareGapsServiceFactory theR4CareGapsProcessorFactory, StringTimePeriodHandler theStringTimePeriodHandler) { + public CareGapsOperationProvider(ICareGapsServiceFactory theR4CareGapsProcessorFactory) { myR4CareGapsProcessorFactory = theR4CareGapsProcessorFactory; - myStringTimePeriodHandler = theStringTimePeriodHandler; } /** @@ -91,6 +87,7 @@ public Parameters careGapsReport(RequestDetails theRequestDetails, CareGapsParam theParams.getPeriodEnd(), theParams.getSubject(), theParams.getStatus(), + // LUKETODO: why can't we have a List @OperationParam? theParams.getMeasureId() == null ? null : theParams.getMeasureId().stream() diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index c987e5c1a78a..aeeb42b33574 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -19,8 +19,8 @@ */ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; @@ -71,31 +71,31 @@ * If 'true', this will return summarized subject bundle with only detectedIssue resource. */ public class CareGapsParams { - @OperationEmbeddedParam( + @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) private final ZonedDateTime myPeriodEnd; - @OperationEmbeddedParam(name = "subject") + @EmbeddedOperationParam(name = "subject") private final String mySubject; - @OperationEmbeddedParam(name = "status") + @EmbeddedOperationParam(name = "status") private final List myStatus; - @OperationEmbeddedParam(name = "measureId") + @EmbeddedOperationParam(name = "measureId") private final List myMeasureId; - @OperationEmbeddedParam(name = "measureIdentifier") + @EmbeddedOperationParam(name = "measureIdentifier") private final List myMeasureIdentifier; - @OperationEmbeddedParam(name = "measureUrl") + @EmbeddedOperationParam(name = "measureUrl") private final List myMeasureUrl; - @OperationEmbeddedParam(name = "nonDocument") + @EmbeddedOperationParam(name = "nonDocument") private final BooleanType myNonDocument; public CareGapsParams( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 536af0e9d803..47f80292455b 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -19,9 +19,9 @@ */ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; @@ -51,41 +51,42 @@ * for the evaluation. This is a non-standard parameter. * myAdditionalData the data bundle containing additional data */ +// LUKETODO: start to integrate this with a clinical reasoning branch public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; - @OperationEmbeddedParam( + @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) private final ZonedDateTime myPeriodEnd; - @OperationEmbeddedParam(name = "reportType") + @EmbeddedOperationParam(name = "reportType") private final String myReportType; - @OperationEmbeddedParam(name = "subject") + @EmbeddedOperationParam(name = "subject") private final String mySubject; - @OperationEmbeddedParam(name = "practitioner") + @EmbeddedOperationParam(name = "practitioner") private final String myPractitioner; - @OperationEmbeddedParam(name = "lastReceivedOn") + @EmbeddedOperationParam(name = "lastReceivedOn") private final String myLastReceivedOn; - @OperationEmbeddedParam(name = "productLine") + @EmbeddedOperationParam(name = "productLine") private final String myProductLine; - @OperationEmbeddedParam(name = "additionalData") + @EmbeddedOperationParam(name = "additionalData") private final Bundle myAdditionalData; - @OperationEmbeddedParam(name = "terminologyEndpoint") + @EmbeddedOperationParam(name = "terminologyEndpoint") private final Endpoint myTerminologyEndpoint; - @OperationEmbeddedParam(name = "parameters") + @EmbeddedOperationParam(name = "parameters") private final Parameters myParameters; public EvaluateMeasureSingleParams( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 4b21b4b1ba73..8a4b64113ca8 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.method.StringTimePeriodHandler; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Measure; @@ -36,13 +35,9 @@ public class MeasureOperationsProvider { private static final Logger ourLog = LoggerFactory.getLogger(MeasureOperationsProvider.class); private final R4MeasureEvaluatorSingleFactory myR4MeasureServiceFactory; - private final StringTimePeriodHandler myStringTimePeriodHandler; - public MeasureOperationsProvider( - R4MeasureEvaluatorSingleFactory theR4MeasureServiceFactory, - StringTimePeriodHandler theStringTimePeriodHandler) { + public MeasureOperationsProvider(R4MeasureEvaluatorSingleFactory theR4MeasureServiceFactory) { myR4MeasureServiceFactory = theR4MeasureServiceFactory; - myStringTimePeriodHandler = theStringTimePeriodHandler; } /** diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 2858978eb87f..f42ca271597a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -79,6 +79,17 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { assertArrayEquals(expectedOutputParams, actualOutputParams); } + @Test + void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { + final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Object[] inputParams = new Object[]{null, null}; + final Object[] expectedOutputParams = new Object[]{new SampleParams(null, null)}; + + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + @Test void happyPathOperationEmbeddedTypesRequestDetailsFirst() { final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java index 3645702dbe3b..3dd8b3dd15e6 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java @@ -3,7 +3,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationEmbeddedParam; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -153,10 +153,10 @@ public String toString() { // Ignore warnings that these classes can be records. Converting them to records will make the tests fail static class SampleParams { - @OperationEmbeddedParam(name = "param1") + @EmbeddedOperationParam(name = "param1") private final String myParam1; - @OperationEmbeddedParam(name = "param2") + @EmbeddedOperationParam(name = "param2") private final List myParam2; public SampleParams(String myParam1, List myParam2) { @@ -195,10 +195,10 @@ public String toString() { } static class ParamsWithTypeConversion { - @OperationEmbeddedParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) + @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @OperationEmbeddedParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) private final ZonedDateTime myPeriodEnd; public ParamsWithTypeConversion(ZonedDateTime myPeriodStart, ZonedDateTime myPeriodEnd) { @@ -241,13 +241,13 @@ static class SampleParamsWithIdParam { @IdParam private final IdType myId; - @OperationEmbeddedParam(name = "param1") + @EmbeddedOperationParam(name = "param1") private final String myParam1; - @OperationEmbeddedParam(name = "param2") + @EmbeddedOperationParam(name = "param2") private final List myParam2; - @OperationEmbeddedParam(name = "param3") + @EmbeddedOperationParam(name = "param3") private final BooleanType myParam3; public SampleParamsWithIdParam(IdType theId, String theParam1, List theParam2, BooleanType theParam3) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 9d633dddb052..1f1e0f1aec47 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -105,11 +105,11 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class) ); assertThat(resourceParameters) @@ -123,12 +123,12 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class) ); assertThat(resourceParameters) @@ -142,11 +142,11 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, RequestDetailsParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), new RequestDetailsParameterToAssert() ); @@ -161,13 +161,13 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class) ); assertThat(resourceParameters) @@ -181,11 +181,11 @@ void paramsConversionZonedDateTime() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( - new OperationEmbeddedParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class), - new OperationEmbeddedParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class) ); assertThat(resourceParameters) @@ -384,12 +384,12 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I return true; } - if (theExpectedParameter instanceof OperationEmbeddedParameterToAssert expectedOperationEmbeddedParameter && theActualParameter instanceof OperationEmbeddedParameter actualOperationEmbeddedParameter) { - assertThat(actualOperationEmbeddedParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationEmbeddedParameter.myContext().getVersion().getVersion()); - assertThat(actualOperationEmbeddedParameter.getName()).isEqualTo(expectedOperationEmbeddedParameter.myName()); - assertThat(actualOperationEmbeddedParameter.getParamType()).isEqualTo(expectedOperationEmbeddedParameter.myParamType()); - assertThat(actualOperationEmbeddedParameter.getInnerCollectionType()).isEqualTo(expectedOperationEmbeddedParameter.myInnerCollectionType()); - assertThat(actualOperationEmbeddedParameter.getSourceType()).isEqualTo(expectedOperationEmbeddedParameter.myTypeToConvertFrom()); + if (theExpectedParameter instanceof EmbeddedOperationParameterToAssert expectedEmbeddedOperationParameter && theActualParameter instanceof EmbeddedOperationParameter actualEmbeddedOperationParameter) { + assertThat(actualEmbeddedOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedEmbeddedOperationParameter.myContext().getVersion().getVersion()); + assertThat(actualEmbeddedOperationParameter.getName()).isEqualTo(expectedEmbeddedOperationParameter.myName()); + assertThat(actualEmbeddedOperationParameter.getParamType()).isEqualTo(expectedEmbeddedOperationParameter.myParamType()); + assertThat(actualEmbeddedOperationParameter.getInnerCollectionType()).isEqualTo(expectedEmbeddedOperationParameter.myInnerCollectionType()); + assertThat(actualEmbeddedOperationParameter.getSourceType()).isEqualTo(expectedEmbeddedOperationParameter.myTypeToConvertFrom()); return true; } @@ -415,7 +415,7 @@ private record OperationParameterToAssert( String myParamType) implements IParameterToAssert { } - private record OperationEmbeddedParameterToAssert( + private record EmbeddedOperationParameterToAssert( FhirContext myContext, String myName, String myOperationName, From 69e1411bc0c63646d12bdf1c5b1c88fbb5f5be84 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 15:00:11 -0500 Subject: [PATCH 51/75] Integrate with clinical-reasoning master. Introduce new annotations meant to replace the existing one and to use OperationParam. First steps. --- .../annotation/EmbeddableOperationParams.java | 11 +++++ .../annotation/EmbeddedOperationParams.java | 11 +++++ .../fhir/rest/server/method/MethodUtil.java | 48 +++++++++++++++++++ .../measure/EvaluateMeasureSingleParams.java | 29 ++++++----- .../r4/measure/MeasureOperationsProvider.java | 4 +- ...thodBindingMethodParameterBuilderTest.java | 42 ++++++++-------- ...EmbeddedParamsInnerClassesAndMethods.java} | 26 ++++++---- .../rest/server/method/MethodUtilTest.java | 26 +++++----- .../method/OperationMethodBindingTest.java | 18 +++---- pom.xml | 3 +- 10 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java rename hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/{InnerClassesAndMethods.java => EmbeddedParamsInnerClassesAndMethods.java} (91%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java new file mode 100644 index 000000000000..fd059b3bb6b9 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java @@ -0,0 +1,11 @@ +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(value = {ElementType.TYPE}) +public @interface EmbeddableOperationParams {} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java new file mode 100644 index 000000000000..2ba40ca19384 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java @@ -0,0 +1,11 @@ +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(value = {ElementType.PARAMETER}) +public @interface EmbeddedOperationParams {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4299fe311f7f..e4cc8b20204b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -32,6 +32,7 @@ import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Elements; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; @@ -199,6 +200,7 @@ public static List getResourceParameters( param = new SearchTotalModeParameter(); } else { final Operation op = methodToUse.getAnnotation(Operation.class); + // LUKETODO: delete this after all existing providers have migrated. // There are no annotations on this parameter, so we check to see if the parameter class has fields // annotated OperationEmbeddedParam if (nextParameterAnnotations.length == 0) { @@ -392,6 +394,52 @@ public static List getResourceParameters( } parameterType = newParameterType; } + } else if (nextAnnotation instanceof EmbeddedOperationParams) { + final List> operationEmbeddedTypes = + ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + methodToUse, EmbeddedOperationParam.class); + + if (op == null) { + throw new ConfigurationException(Msg.code(846192641) + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + + methodToUse.toGenericString()); + } + + if (operationEmbeddedTypes.size() > 1) { + throw new ConfigurationException(String.format( + "%sOnly one type with embedded params is supported for now for method: %s", + Msg.code(9999927), methodToUse.getName())); + } + + if (!operationEmbeddedTypes.isEmpty()) { + final EmbeddedParameterConverter embeddedParameterConverter = + new EmbeddedParameterConverter( + theContext, theMethod, op, operationEmbeddedTypes.get(0)); + + final List outerContexts = + embeddedParameterConverter.convert(); + + for (EmbeddedParameterConverterContext outerContext : outerContexts) { + if (outerContext.getParameter() != null) { + parameters.add(outerContext.getParameter()); + } + final ParamInitializationContext paramContext = outerContext.getParamContext(); + + if (paramContext != null) { + paramContexts.add(paramContext); + + // N.B. This a hack used only to pass the null check below, which is crucial to the + // non-embedded params logic + param = paramContext.getParam(); + } + } + } else { + // More than likely this will result in the param == null Exception below + ourLog.warn( + "Method '{}' has no parameters with annotations. Don't know how to handle this parameter", + methodToUse.getName()); + } + ourLog.info("1234: NEW CODE PATH!!!!!"); } else if (nextAnnotation instanceof Validate.Mode) { if (!parameterType.equals(ValidationModeEnum.class)) { throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 47f80292455b..9bf36b79cf58 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; @@ -52,10 +53,13 @@ * myAdditionalData the data bundle containing additional data */ // LUKETODO: start to integrate this with a clinical reasoning branch +// LUKETODO: make code use or at least validate this annotation +@EmbeddableOperationParams public class EvaluateMeasureSingleParams { @IdParam private final IdType myId; + // LUKETODO: OperationParam @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, @@ -89,6 +93,8 @@ public class EvaluateMeasureSingleParams { @EmbeddedOperationParam(name = "parameters") private final Parameters myParameters; + // LUKETODO: embedded factory constructor annoation + // LUKETODO: annotations on constructor parameters instead public EvaluateMeasureSingleParams( IdType theId, ZonedDateTime thePeriodStart, @@ -115,17 +121,18 @@ public EvaluateMeasureSingleParams( } private EvaluateMeasureSingleParams(Builder builder) { - this.myId = builder.myId; - this.myPeriodStart = builder.myPeriodStart; - this.myPeriodEnd = builder.myPeriodEnd; - this.myReportType = builder.myReportType; - this.mySubject = builder.mySubject; - this.myPractitioner = builder.myPractitioner; - this.myLastReceivedOn = builder.myLastReceivedOn; - this.myProductLine = builder.myProductLine; - this.myAdditionalData = builder.myAdditionalData; - this.myTerminologyEndpoint = builder.myTerminologyEndpoint; - this.myParameters = builder.myParameters; + this( + builder.myId, + builder.myPeriodStart, + builder.myPeriodEnd, + builder.myReportType, + builder.mySubject, + builder.myPractitioner, + builder.myLastReceivedOn, + builder.myProductLine, + builder.myAdditionalData, + builder.myTerminologyEndpoint, + builder.myParameters); } public IdType getId() { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 8a4b64113ca8..6ff77dfda5c4 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.cr.r4.R4MeasureEvaluatorSingleFactory; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -54,7 +55,8 @@ public MeasureOperationsProvider(R4MeasureEvaluatorSingleFactory theR4MeasureSer * @return the calculated MeasureReport */ @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE, idempotent = true, type = Measure.class) - public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) + public MeasureReport evaluateMeasure( + @EmbeddedOperationParams EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) throws InternalErrorException, FHIRException { return myR4MeasureServiceFactory .create(theRequestDetails) diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index f42ca271597a..b2050ca3e815 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -4,7 +4,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; @@ -18,14 +18,14 @@ import java.time.ZoneOffset; import java.util.List; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithTypeConversion; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithoutAnnotations; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithoutAnnotations; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -41,7 +41,7 @@ class BaseMethodBindingMethodParameterBuilderTest { private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); - private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); + private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); // LUKETODO: wrong params // LUKETODO: wrong param order @@ -50,7 +50,7 @@ class BaseMethodBindingMethodParameterBuilderTest { @Test void happyPathOperationParamsEmptyParams() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); @@ -60,7 +60,7 @@ void happyPathOperationParamsEmptyParams() { @Test void happyPathOperationParamsNonEmptyParams() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); @@ -70,7 +70,7 @@ void happyPathOperationParamsNonEmptyParams() { @Test void happyPathOperationEmbeddedTypesNoRequestDetails() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; @@ -81,7 +81,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { @Test void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{null, null}; final Object[] expectedOutputParams = new Object[]{new SampleParams(null, null)}; @@ -92,7 +92,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { @Test void happyPathOperationEmbeddedTypesRequestDetailsFirst() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; @@ -103,7 +103,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() { @Test void happyPathOperationEmbeddedTypesRequestDetailsLast() { - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; @@ -116,7 +116,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { @Disabled void happyPathOperationEmbeddedTypesWithIdType() { final IdType id = new IdType(); - final Method sampleMethod = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; @@ -134,7 +134,7 @@ void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { @Test void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws NoSuchMethodException { - final Method sampleMethod = InnerClassesAndMethods.class.getDeclaredMethod(InnerClassesAndMethods.SUPER_SIMPLE); + final Method sampleMethod = EmbeddedParamsInnerClassesAndMethods.class.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { buildMethodParams(sampleMethod, REQUEST_DETAILS, null); @@ -148,7 +148,7 @@ void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException @Test void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, + final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, RequestDetails.class, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { @@ -160,7 +160,7 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( @Test @Disabled void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); + final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; @@ -171,7 +171,7 @@ void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternal @Test void paramsConversionZonedDateTime() { - final Method method = myInnerClassesAndMethods.getDeclaredMethod(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); + final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); final Object[] inputParams = new Object[]{"2024-01-01", "2025-01-01"}; final Object[] expectedOutputParams = new Object[]{ diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java similarity index 91% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java index 3dd8b3dd15e6..a095c669b5c3 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/InnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server.method; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; @@ -31,7 +33,7 @@ // This class lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes -class InnerClassesAndMethods { +class EmbeddedParamsInnerClassesAndMethods { static final String SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS = "sampleMethodEmbeddedTypeMultipleRequestDetails"; static final String SUPER_SIMPLE = "superSimple"; @@ -111,6 +113,7 @@ public MeasureReport sampleMethodOperationParams( return new MeasureReport(null, null, null, null); } + @EmbeddableOperationParams static class ParamsWithoutAnnotations { private final String myParam1; private final List myParam2; @@ -152,6 +155,7 @@ public String toString() { } // Ignore warnings that these classes can be records. Converting them to records will make the tests fail + @EmbeddableOperationParams static class SampleParams { @EmbeddedOperationParam(name = "param1") private final String myParam1; @@ -194,6 +198,7 @@ public String toString() { } } + @EmbeddableOperationParams static class ParamsWithTypeConversion { @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) private final ZonedDateTime myPeriodStart; @@ -237,6 +242,7 @@ public String toString() { } // Ignore warnings that these classes can be records. Converting them to records will make the tests fail + @EmbeddableOperationParams static class SampleParamsWithIdParam { @IdParam private final IdType myId; @@ -300,51 +306,51 @@ public String toString() { } @Operation(name="sampleMethodEmbeddedTypeRequestDetailsFirst") - String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, SampleParams theParams) { + String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, @EmbeddedOperationParams SampleParams theParams) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); } @Operation(name="sampleMethodEmbeddedTypeRequestDetailsLast") - String sampleMethodEmbeddedTypeRequestDetailsLast(SampleParams theParams, RequestDetails theRequestDetails) { + String sampleMethodEmbeddedTypeRequestDetailsLast(@EmbeddedOperationParams SampleParams theParams, RequestDetails theRequestDetails) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); } @Operation(name="sampleMethodEmbeddedTypeNoRequestDetails") - String sampleMethodEmbeddedTypeNoRequestDetails(SampleParams theParams) { + String sampleMethodEmbeddedTypeNoRequestDetails(@EmbeddedOperationParams SampleParams theParams) { // return something arbitrary return theParams.getParam1(); } @Operation(name="simpleMethodWithParamsConversion") - String simpleMethodWithParamsConversion(ParamsWithTypeConversion theParams) { + String simpleMethodWithParamsConversion(@EmbeddedOperationParams ParamsWithTypeConversion theParams) { // return something arbitrary return theParams.getPeriodStart().toString(); } - String sampleMethodParamNoEmbeddedType(ParamsWithoutAnnotations theParams) { + String sampleMethodParamNoEmbeddedType(@EmbeddedOperationParams ParamsWithoutAnnotations theParams) { // return something arbitrary return theParams.getParam1(); } - String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, SampleParams theParams, RequestDetails theRequestDetails2) { + String sampleMethodEmbeddedTypeMultipleRequestDetails(RequestDetails theRequestDetails1, @EmbeddedOperationParams SampleParams theParams, RequestDetails theRequestDetails2) { // return something arbitrary return theRequestDetails1.getId().getValue() + theParams.getParam1() + theRequestDetails2.getId().getValue(); } - String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, SampleParamsWithIdParam theParams) { + String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theRequestDetails, @EmbeddedOperationParams SampleParamsWithIdParam theParams) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); } - String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { + String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(@EmbeddedOperationParams SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); } @Operation(name="sampleMethodEmbeddedTypeNoRequestDetailsWithIdType", type = Measure.class) - MeasureReport sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(SampleParamsWithIdParam theParams) { + MeasureReport sampleMethodEmbeddedTypeNoRequestDetailsWithIdType(@EmbeddedOperationParams SampleParamsWithIdParam theParams) { // return something arbitrary return new MeasureReport(null, null, null, null); } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 1f1e0f1aec47..a9df8d100b13 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -3,9 +3,9 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.ParamsWithTypeConversion; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParams; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.junit.jupiter.api.Disabled; @@ -17,14 +17,14 @@ import java.util.Collection; import java.util.List; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; @@ -39,7 +39,7 @@ class MethodUtilTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); - private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); + private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); private final Object myProvider = new Object(); @@ -344,7 +344,7 @@ void getResourceParameters_withMultipleAnnotations_shouldReturnCorrectParameters private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( ourFhirContext, - myInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), + myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), myProvider); } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 32ccde232f96..448f2f0b693a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.rest.server.method; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.EXPAND; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.OP_INSTANCE_OR_TYPE; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.OP_INSTANCE_OR_TYPE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static org.junit.jupiter.api.Assertions.*; import ca.uhn.fhir.context.ConfigurationException; @@ -13,8 +13,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.PatientProvider; -import ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SampleParamsWithIdParam; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.PatientProvider; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -34,7 +34,7 @@ class OperationMethodBindingTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); - private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); + private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); private Method myMethod; private Operation myOperation; @@ -83,7 +83,7 @@ void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact( @Test void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() { - init(InnerClassesAndMethods.SIMPLE_OPERATION); + init(EmbeddedParamsInnerClassesAndMethods.SIMPLE_OPERATION); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.PUT); @@ -162,7 +162,7 @@ void methodWithIdParamButNoIIdType() { } private void init(String theMethodName, Class... theParamClasses) { - myMethod = myInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses); + myMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses); myOperation = myMethod.getAnnotation(Operation.class); } } diff --git a/pom.xml b/pom.xml index e8240dc45cf5..d717ab5e57f3 100644 --- a/pom.xml +++ b/pom.xml @@ -1087,7 +1087,8 @@ 1.0.8 - 3.17.0 + + 3.18.0-SNAPSHOT 5.4.1 From 365b7465ba2363e6892fbe63c76bcac8e850f37a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 16:21:03 -0500 Subject: [PATCH 52/75] Rename classes. Add annotations to caregaps and multimeasures. Start migrating OperationParameter and OperationParam to have parity with EmbeddedOperationParameter and EmbeddedOperationParam. --- .../annotation/EmbeddableOperationParams.java | 19 ++++++ .../annotation/EmbeddedOperationParam.java | 2 +- .../annotation/EmbeddedOperationParams.java | 19 ++++++ .../fhir/rest/annotation/OperationParam.java | 14 +++++ ....java => OperationParameterRangeType.java} | 2 +- ...seMethodBindingMethodParameterBuilder.java | 4 +- .../method/EmbeddedOperationParameter.java | 16 +++-- .../server/method/EmbeddedOperationUtils.java | 12 ++-- .../fhir/rest/server/method/MethodUtil.java | 61 +++--------------- .../server/method/OperationParameter.java | 63 ++++++++++++++----- .../ValidateMethodBindingDstu2Plus.java | 17 ++--- .../r4/measure/CareGapsOperationProvider.java | 3 +- .../fhir/cr/r4/measure/CareGapsParams.java | 8 ++- .../measure/EvaluateMeasureSingleParams.java | 6 +- .../EmbeddedParamsInnerClassesAndMethods.java | 6 +- .../rest/server/method/MethodUtilTest.java | 45 +++++++------ 16 files changed, 179 insertions(+), 118 deletions(-) rename hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/{EmbeddedParameterRangeType.java => OperationParameterRangeType.java} (95%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java index fd059b3bb6b9..f209297fd9e7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java @@ -1,3 +1,22 @@ +/*- + * #%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; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java index 1c79661d97f5..457f9e59cfdc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java @@ -92,7 +92,7 @@ * @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. */ - EmbeddedParameterRangeType rangeType() default EmbeddedParameterRangeType.NOT_APPLICABLE; + OperationParameterRangeType rangeType() default OperationParameterRangeType.NOT_APPLICABLE; /** * Optionally specifies the type of the parameter as a string, such as Coding or diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java index 2ba40ca19384..09478ed8855c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java @@ -1,3 +1,22 @@ +/*- + * #%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; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index 747e7bc0da59..a179d0c878f5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -73,6 +73,20 @@ */ Class 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 Coding or * base64Binary. This can be useful if you want to use a generic interface type diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java similarity index 95% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java index c0298bdb5c2f..9e38850cbba4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedParameterRangeType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java @@ -23,7 +23,7 @@ * Used to indicate whether an {@link EmbeddedOperationParam} 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 EmbeddedParameterRangeType { +public enum OperationParameterRangeType { START, END, NOT_APPLICABLE diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index c0c9d79fa3da..f14a0b5a3af3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -22,7 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -246,7 +246,7 @@ private Object convertParamIfNeeded( final EmbeddedOperationParam embeddedParamAtIndex = (EmbeddedOperationParam) annotation; final Class paramClassAtIndex = paramAtIndex.getClass(); - final EmbeddedParameterRangeType rangeType = embeddedParamAtIndex.rangeType(); + final OperationParameterRangeType rangeType = embeddedParamAtIndex.rangeType(); final Parameter constructorParameter = theConstructorParameters[theIndex]; final Class constructorParameterType = constructorParameter.getType(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java index 2bef7e73fdbc..6f075f769d14 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java @@ -27,7 +27,7 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.QualifiedParamList; @@ -54,7 +54,6 @@ import java.util.function.Consumer; import java.util.*; -import static ca.uhn.fhir.rest.server.method.OperationParameter.REQUEST_CONTENTS_USERDATA_KEY; import static org.apache.commons.lang3.StringUtils.isNotBlank; // LUKETODO: consider deleting whatever code may be unused @@ -90,7 +89,7 @@ public class EmbeddedOperationParameter implements IParameter { private final List myExampleValues; private final Class mySourceType; // LUKETODO: just pass the whole thing? - private final EmbeddedParameterRangeType myRengeType; + private final OperationParameterRangeType myRengeType; EmbeddedOperationParameter( FhirContext theCtx, @@ -101,7 +100,7 @@ public class EmbeddedOperationParameter implements IParameter { String theDescription, List theExampleValues, Class theSourceType, - EmbeddedParameterRangeType theRengeType) { + OperationParameterRangeType theRangeType) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; @@ -114,7 +113,7 @@ public class EmbeddedOperationParameter implements IParameter { } else { mySourceType = theSourceType; } - myRengeType = theRengeType; + myRengeType = theRangeType; List exampleValues = new ArrayList<>(); if (theExampleValues != null) { @@ -180,6 +179,11 @@ public Class getSourceType() { return mySourceType; } + @VisibleForTesting + public OperationParameterRangeType getRangeType() { + return myRengeType; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( @@ -262,7 +266,7 @@ public void initializeTypes( // LUKETODO: test the rangeType NOT_APPLICABLE scenario final String error = String.format( "%sInvalid type for @OperationEmbeddedParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", - Msg.code(999991), theMethod.getName(), mySourceType, myParameterType, myRengeType); + Msg.code(999991), theMethod.getName(), mySourceType, myParameterType.getName(), myRengeType); throw new ConfigurationException(error); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 2bf3919804a9..3fd3951f8625 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -22,7 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; @@ -118,9 +118,9 @@ private static Boolean isValidSourceTypeConversion( if (annotation instanceof EmbeddedOperationParam) { final EmbeddedOperationParam embeddedOperationParam = (EmbeddedOperationParam) annotation; - final EmbeddedParameterRangeType embeddedParameterRangeType = embeddedOperationParam.rangeType(); + final OperationParameterRangeType operationParameterRangeType = embeddedOperationParam.rangeType(); - if (isValidSourceTypeConversion(methodParamClass, constructorParamType, embeddedParameterRangeType)) { + if (isValidSourceTypeConversion(methodParamClass, constructorParamType, operationParameterRangeType)) { return true; } } @@ -134,14 +134,14 @@ private static Boolean isValidSourceTypeConversion( * * @param theSourceType The source type for the class, which can be different from the declared type * @param theTargetType The target type for the class, which can be different from the source type - * @param theEmbeddedParameterRangeType Whether the embedded parameter is a range and if so, start or end + * @param theOperationParameterRangeType Whether the embedded parameter is a range and if so, start or end * @return true if the type conversion is supported */ static boolean isValidSourceTypeConversion( - Class theSourceType, Class theTargetType, EmbeddedParameterRangeType theEmbeddedParameterRangeType) { + Class theSourceType, Class theTargetType, OperationParameterRangeType theOperationParameterRangeType) { return String.class == theSourceType && ZonedDateTime.class == theTargetType - && EmbeddedParameterRangeType.NOT_APPLICABLE != theEmbeddedParameterRangeType; + && OperationParameterRangeType.NOT_APPLICABLE != theOperationParameterRangeType; } private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index e4cc8b20204b..224b135942c7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -40,6 +40,7 @@ import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Patch; import ca.uhn.fhir.rest.annotation.RawParam; @@ -200,54 +201,6 @@ public static List getResourceParameters( param = new SearchTotalModeParameter(); } else { final Operation op = methodToUse.getAnnotation(Operation.class); - // LUKETODO: delete this after all existing providers have migrated. - // There are no annotations on this parameter, so we check to see if the parameter class has fields - // annotated OperationEmbeddedParam - if (nextParameterAnnotations.length == 0) { - final List> operationEmbeddedTypes = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, EmbeddedOperationParam.class); - - if (op == null) { - throw new ConfigurationException(Msg.code(846192641) - + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " - + methodToUse.toGenericString()); - } - - if (operationEmbeddedTypes.size() > 1) { - throw new ConfigurationException(String.format( - "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - - if (!operationEmbeddedTypes.isEmpty()) { - final EmbeddedParameterConverter embeddedParameterConverter = new EmbeddedParameterConverter( - theContext, theMethod, op, operationEmbeddedTypes.get(0)); - - final List outerContexts = - embeddedParameterConverter.convert(); - - for (EmbeddedParameterConverterContext outerContext : outerContexts) { - if (outerContext.getParameter() != null) { - parameters.add(outerContext.getParameter()); - } - final ParamInitializationContext paramContext = outerContext.getParamContext(); - - if (paramContext != null) { - paramContexts.add(paramContext); - - // N.B. This a hack used only to pass the null check below, which is crucial to the - // non-embedded params logic - param = paramContext.getParam(); - } - } - } else { - // More than likely this will result in the param == null Exception below - ourLog.warn( - "Method '{}' has no parameters with annotations. Don't know how to handle this parameter", - methodToUse.getName()); - } - } // else there are no embedded params and let execution fall to the for loop below for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; @@ -375,7 +328,9 @@ public static List getResourceParameters( operationParam.min(), operationParam.max(), description, - examples); + examples, + operationParam.sourceType(), + operationParam.rangeType()); if (isNotBlank(operationParam.typeName())) { BaseRuntimeElementDefinition elementDefinition = theContext.getElementDefinition(operationParam.typeName()); @@ -455,7 +410,9 @@ public static List getResourceParameters( 0, 1, description, - examples) + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) .setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { @@ -492,7 +449,9 @@ public Object outgoingClient(Object theObject) { 0, 1, description, - examples) + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) .setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 4e5e40f4a3f3..89ec98b92d2c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -36,6 +36,7 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.ValidationModeEnum; @@ -86,12 +87,15 @@ public class OperationParameter implements IParameter { private Class myInnerCollectionType; private int myMax; - private int myMin; + private final int myMin; private Class myParameterType; private String myParamType; private SearchParameter mySearchParameterBinding; - private String myDescription; - private List myExampleValues; + private final String myDescription; + private final List myExampleValues; + private final Class mySourceType; + // LUKETODO: just pass the whole thing? + private final OperationParameterRangeType myRangeType; OperationParameter( FhirContext theCtx, @@ -100,13 +104,22 @@ public class OperationParameter implements IParameter { int theMin, int theMax, String theDescription, - List theExampleValues) { + List theExampleValues, + Class theSourceType, + OperationParameterRangeType theRangeType) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; myMax = theMax; myContext = theCtx; myDescription = theDescription; + // LUKETODO: is this wise? + if (theSourceType == Void.class && myParameterType != null) { + mySourceType = myParameterType; + } else { + mySourceType = theSourceType; + } + myRangeType = theRangeType; List exampleValues = new ArrayList<>(); if (theExampleValues != null) { @@ -118,7 +131,7 @@ public class OperationParameter implements IParameter { @SuppressWarnings({"rawtypes", "unchecked"}) private void addValueToList(List matchingParamValues, Object values) { if (values != null) { - if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { + if (BaseAndListParam.class.isAssignableFrom(myParameterType) && !matchingParamValues.isEmpty()) { BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); BaseAndListParam newAndList = (BaseAndListParam) values; for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { @@ -162,11 +175,20 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + Class getSourceType() { + return mySourceType; + } + @VisibleForTesting String getOperationName() { return myOperationName; } + public OperationParameterRangeType getRangeType() { + return myRangeType; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( @@ -208,6 +230,7 @@ public void initializeTypes( myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) + || String.class.equals(mySourceType) || isSearchParam || ValidationModeEnum.class.equals(myParameterType); @@ -215,7 +238,9 @@ public void initializeTypes( * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We * should probably clean this up.. */ - if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { + if (!myParameterType.equals(IBase.class) + && !myParameterType.equals(String.class) + && !EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRangeType)) { if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { @@ -242,8 +267,12 @@ public void initializeTypes( myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); myConverter = new OperationParamConverter(); } else { - throw new ConfigurationException(Msg.code(361) + "Invalid type for @OperationParam on method " - + theMethod + ": " + myParameterType.getName()); + // LUKETODO: claim new code + // LUKETODO: test the rangeType NOT_APPLICABLE scenario + final String error = String.format( + "%sInvalid type for @OperationEmbeddedParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", + Msg.code(361), theMethod.getName(), mySourceType, myParameterType.getName(), myRangeType); + throw new ConfigurationException(error); } } } @@ -313,7 +342,7 @@ private void translateQueryParametersIntoServerArgumentForGet( RequestDetails theRequest, List matchingParamValues) { if (mySearchParameterBinding != null) { - List params = new ArrayList(); + List params = new ArrayList<>(); String nameWithQualifierColon = myName + ":"; for (String nextParamName : theRequest.getParameters().keySet()) { @@ -376,7 +405,9 @@ private void translateQueryParametersIntoServerArgumentForGet( matchingParamValues.add(param); }); - } else if (String.class.isAssignableFrom(myParameterType)) { + // LUKETODO: comment to explain + // LUKETODO: call EmbeddedOperationUtils.isValidSourceTypeConversion ???? + } else if (String.class.isAssignableFrom(myParameterType) || String.class.equals(mySourceType)) { matchingParamValues.addAll(Arrays.asList(paramValues)); @@ -449,7 +480,7 @@ private void translateQueryParametersIntoServerArgumentForPost( List values = paramChildAccessor.getValues(requestContents); for (IBase nextParameter : values) { List nextNames = nameChild.getAccessor().getValues(nextParameter); - if (nextNames != null && nextNames.size() > 0) { + if (nextNames != null && !nextNames.isEmpty()) { IPrimitiveType nextName = (IPrimitiveType) nextNames.get(0); if (myName.equals(nextName.getValueAsString())) { @@ -460,9 +491,9 @@ private void translateQueryParametersIntoServerArgumentForPost( valueChild.getAccessor().getValues(nextParameter); List paramResources = resourceChild.getAccessor().getValues(nextParameter); - if (paramValues != null && paramValues.size() > 0) { + if (paramValues != null && !paramValues.isEmpty()) { tryToAddValues(paramValues, matchingParamValues); - } else if (paramResources != null && paramResources.size() > 0) { + } else if (paramResources != null && !paramResources.isEmpty()) { tryToAddValues(paramResources, matchingParamValues); } } @@ -488,7 +519,9 @@ private void tryToAddValues(List theParamValues, List theMatching if (myConverter != null) { nextValue = myConverter.incomingServer(nextValue); } - if (myParameterType.equals(String.class)) { + // LUKETODO: test this + if (myParameterType.equals(String.class) + || EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRangeType)) { if (nextValue instanceof IPrimitiveType) { IPrimitiveType source = (IPrimitiveType) nextValue; theMatchingParamValues.add(source.getValueAsString()); @@ -526,7 +559,7 @@ public List getExampleValues() { return myExampleValues; } - interface IOperationParamConverter { + public interface IOperationParamConverter { Object incomingServer(Object theObject); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java index 756b11f44f2e..9768038425ab 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; @@ -70,13 +71,15 @@ public ValidateMethodBindingDstu2Plus( String description = ParametersUtil.extractDescription(parameterAnnotations); List examples = ParametersUtil.extractExamples(parameterAnnotations); OperationParameter parameter = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_RESOURCE, - 0, - 1, - description, - examples); + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_RESOURCE, + 0, + 1, + description, + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE); parameter.initializeTypes(theMethod, null, null, parameterType); newParams.add(parameter); } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index f786bbff4a14..20d95d69f0ba 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.cr.r4.ICareGapsServiceFactory; import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; @@ -78,7 +79,7 @@ public CareGapsOperationProvider(ICareGapsServiceFactory theR4CareGapsProcessorF value = "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) - public Parameters careGapsReport(RequestDetails theRequestDetails, CareGapsParams theParams) { + public Parameters careGapsReport(RequestDetails theRequestDetails, @EmbeddedOperationParams CareGapsParams theParams) { return myR4CareGapsProcessorFactory .create(theRequestDetails) diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index aeeb42b33574..648f72933f40 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -19,8 +19,9 @@ */ package ca.uhn.fhir.cr.r4.measure; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; @@ -70,14 +71,15 @@ * myNonDocument defaults to 'false' which returns standard 'document' bundle for `$care-gaps`. * If 'true', this will return summarized subject bundle with only detectedIssue resource. */ +@EmbeddableOperationParams public class CareGapsParams { @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, - rangeType = EmbeddedParameterRangeType.START) + rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; @EmbeddedOperationParam(name = "subject") diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 9bf36b79cf58..52bda839b200 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -21,7 +21,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; @@ -63,10 +63,10 @@ public class EvaluateMeasureSingleParams { @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, - rangeType = EmbeddedParameterRangeType.START) + rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; @EmbeddedOperationParam(name = "reportType") diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java index a095c669b5c3..14e59c254de3 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -2,7 +2,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; -import ca.uhn.fhir.rest.annotation.EmbeddedParameterRangeType; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; @@ -200,10 +200,10 @@ public String toString() { @EmbeddableOperationParams static class ParamsWithTypeConversion { - @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = EmbeddedParameterRangeType.START) + @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = EmbeddedParameterRangeType.END) + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; public ParamsWithTypeConversion(ZonedDateTime myPeriodStart, ZonedDateTime myPeriodEnd) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index a9df8d100b13..6217acae5438 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; @@ -69,9 +70,9 @@ void sampleMethodOperationParams() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), - new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -89,9 +90,9 @@ void sampleMethodOperationParamsWithFhirTypes() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class,null), - new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class,null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -108,8 +109,8 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -127,8 +128,8 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -145,8 +146,8 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), new RequestDetailsParameterToAssert() ); @@ -165,9 +166,9 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { final List expectedParameters = List.of( new NullParameterToAssert(), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new EmbeddedOperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -184,8 +185,8 @@ void paramsConversionZonedDateTime() { assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class), - new EmbeddedOperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class) + new EmbeddedOperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), + new EmbeddedOperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) ); assertThat(resourceParameters) @@ -380,6 +381,8 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I assertThat(actualOperationParameter.getName()).isEqualTo(expectedOperationParameter.myName()); assertThat(actualOperationParameter.getParamType()).isEqualTo(expectedOperationParameter.myParamType()); assertThat(actualOperationParameter.getInnerCollectionType()).isEqualTo(expectedOperationParameter.myInnerCollectionType()); + assertThat(actualOperationParameter.getSourceType()).isEqualTo(expectedOperationParameter.myTypeToConvertFrom()); + assertThat(actualOperationParameter.getRangeType()).isEqualTo(expectedOperationParameter.myRangeType()); return true; } @@ -390,6 +393,7 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I assertThat(actualEmbeddedOperationParameter.getParamType()).isEqualTo(expectedEmbeddedOperationParameter.myParamType()); assertThat(actualEmbeddedOperationParameter.getInnerCollectionType()).isEqualTo(expectedEmbeddedOperationParameter.myInnerCollectionType()); assertThat(actualEmbeddedOperationParameter.getSourceType()).isEqualTo(expectedEmbeddedOperationParameter.myTypeToConvertFrom()); + assertThat(actualEmbeddedOperationParameter.getRangeType()).isEqualTo(expectedEmbeddedOperationParameter.myRangeType()); return true; } @@ -412,7 +416,9 @@ private record OperationParameterToAssert( @SuppressWarnings("rawtypes") Class myInnerCollectionType, Class myParameterType, - String myParamType) implements IParameterToAssert { + String myParamType, + Class myTypeToConvertFrom, + OperationParameterRangeType myRangeType) implements IParameterToAssert { } private record EmbeddedOperationParameterToAssert( @@ -423,6 +429,7 @@ private record EmbeddedOperationParameterToAssert( Class myInnerCollectionType, Class myParameterType, String myParamType, - Class myTypeToConvertFrom) implements IParameterToAssert { + Class myTypeToConvertFrom, + OperationParameterRangeType myRangeType) implements IParameterToAssert { } } From 33d818f3c2fc3979a0a2f64fc76b352d9f078e64 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Jan 2025 16:22:56 -0500 Subject: [PATCH 53/75] Spotless. --- .../method/EmbeddedOperationParameter.java | 2 +- .../server/method/EmbeddedOperationUtils.java | 4 +++- .../fhir/rest/server/method/MethodUtil.java | 8 ++++---- .../method/ValidateMethodBindingDstu2Plus.java | 18 +++++++++--------- .../r4/measure/CareGapsOperationProvider.java | 3 ++- .../measure/EvaluateMeasureSingleParams.java | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java index 6f075f769d14..4dad5d418e38 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java @@ -27,9 +27,9 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.ValidationModeEnum; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 3fd3951f8625..c16d4f12e56a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -138,7 +138,9 @@ private static Boolean isValidSourceTypeConversion( * @return true if the type conversion is supported */ static boolean isValidSourceTypeConversion( - Class theSourceType, Class theTargetType, OperationParameterRangeType theOperationParameterRangeType) { + Class theSourceType, + Class theTargetType, + OperationParameterRangeType theOperationParameterRangeType) { return String.class == theSourceType && ZonedDateTime.class == theTargetType && OperationParameterRangeType.NOT_APPLICABLE != theOperationParameterRangeType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 224b135942c7..0be544334fda 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -411,8 +411,8 @@ public static List getResourceParameters( 1, description, examples, - Void.class, - OperationParameterRangeType.NOT_APPLICABLE) + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) .setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { @@ -450,8 +450,8 @@ public Object outgoingClient(Object theObject) { 1, description, examples, - Void.class, - OperationParameterRangeType.NOT_APPLICABLE) + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) .setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java index 9768038425ab..3f651bf125f7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java @@ -71,15 +71,15 @@ public ValidateMethodBindingDstu2Plus( String description = ParametersUtil.extractDescription(parameterAnnotations); List examples = ParametersUtil.extractExamples(parameterAnnotations); OperationParameter parameter = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_RESOURCE, - 0, - 1, - description, - examples, - Void.class, - OperationParameterRangeType.NOT_APPLICABLE); + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_RESOURCE, + 0, + 1, + description, + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE); parameter.initializeTypes(theMethod, null, null, parameterType); newParams.add(parameter); } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 20d95d69f0ba..8ada0d58de65 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -79,7 +79,8 @@ public CareGapsOperationProvider(ICareGapsServiceFactory theR4CareGapsProcessorF value = "Implements the $care-gaps operation found in the Da Vinci DEQM FHIR Implementation Guide which is an extension of the $care-gaps operation found in the FHIR Clinical Reasoning Module.") @Operation(name = ProviderConstants.CR_OPERATION_CARE_GAPS, idempotent = true, type = Measure.class) - public Parameters careGapsReport(RequestDetails theRequestDetails, @EmbeddedOperationParams CareGapsParams theParams) { + public Parameters careGapsReport( + RequestDetails theRequestDetails, @EmbeddedOperationParams CareGapsParams theParams) { return myR4CareGapsProcessorFactory .create(theRequestDetails) diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 52bda839b200..61ecc5ec0064 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; From ac9451b513d7c6f68215f0b657bbf579695f0de0 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 09:01:56 -0500 Subject: [PATCH 54/75] Commit non-breaking changes to migrate towards the new annotations. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 22 ++++++++++++++++ .../server/method/EmbeddedOperationUtils.java | 26 +++++++++++++++++++ .../server/method/OperationParameter.java | 2 +- .../fhir/cr/r4/measure/CareGapsParams.java | 13 ++++++++++ .../measure/EvaluateMeasureSingleParams.java | 15 +++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index b6d5a8b72d3c..f78b805e0714 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -31,6 +31,7 @@ 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; @@ -167,6 +168,27 @@ 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 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)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index c16d4f12e56a..d610b6032fab 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; @@ -35,7 +36,10 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import java.util.stream.IntStream; /** @@ -146,6 +150,28 @@ static boolean isValidSourceTypeConversion( && OperationParameterRangeType.NOT_APPLICABLE != theOperationParameterRangeType; } + public static List> getMethodParamsAnnotatedWithEmbeddedOperationParams(Method theMethod) { + return Arrays.stream(theMethod.getParameterTypes()) + .filter(EmbeddedOperationUtils::hasEmbeddedOperationParamsAnnotation) + .collect(Collectors.toUnmodifiableList()); + } + + private static boolean hasEmbeddedOperationParamsAnnotation(Class theMethodParameterType) { + final Annotation[] annotations = theMethodParameterType.getAnnotations(); + + if (annotations.length == 0) { + return false; + } + + if (annotations.length > 1) { + throw new ConfigurationException(String.format( + "%sInvalid operation embedded parameters. Class has more than one annotation: %s", + Msg.code(9132164), theMethodParameterType)); + } + + return EmbeddedOperationParams.class == annotations[0].annotationType(); + } + private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 89ec98b92d2c..ad44273ba8ac 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -270,7 +270,7 @@ public void initializeTypes( // LUKETODO: claim new code // LUKETODO: test the rangeType NOT_APPLICABLE scenario final String error = String.format( - "%sInvalid type for @OperationEmbeddedParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", + "%sInvalid type for @OperationParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", Msg.code(361), theMethod.getName(), mySourceType, myParameterType.getName(), myRangeType); throw new ConfigurationException(error); } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 648f72933f40..cb9610c493cb 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; @@ -73,6 +74,7 @@ */ @EmbeddableOperationParams public class CareGapsParams { + // LUKETODO: when ready, cut the umbilical cord @EmbeddedOperationParam( name = "periodStart", sourceType = String.class, @@ -101,13 +103,24 @@ public class CareGapsParams { private final BooleanType myNonDocument; public CareGapsParams( + @OperationParam( + name = "periodStart", + sourceType = String.class, + rangeType = OperationParameterRangeType.START) ZonedDateTime thePeriodStart, + @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) ZonedDateTime thePeriodEnd, + @OperationParam(name = "subject") String theSubject, + @OperationParam(name = "status") List theStatus, + @OperationParam(name = "measureId") List theMeasureId, + @OperationParam(name = "measureIdentifier") List theMeasureIdentifier, + @OperationParam(name = "measureUrl") List theMeasureUrl, + @OperationParam(name = "nonDocument") BooleanType theNonDocument) { myPeriodStart = thePeriodStart; myPeriodEnd = thePeriodEnd; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 61ecc5ec0064..6dc8fc885573 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; @@ -96,16 +97,30 @@ public class EvaluateMeasureSingleParams { // LUKETODO: embedded factory constructor annoation // LUKETODO: annotations on constructor parameters instead public EvaluateMeasureSingleParams( + @IdParam IdType theId, + @OperationParam( + name = "periodStart", + sourceType = String.class, + rangeType = OperationParameterRangeType.START) ZonedDateTime thePeriodStart, + @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) ZonedDateTime thePeriodEnd, + @OperationParam(name = "reportType") String theReportType, + @OperationParam(name = "subject") String theSubject, + @OperationParam(name = "practitioner") String thePractitioner, + @OperationParam(name = "lastReceivedOn") String theLastReceivedOn, + @OperationParam(name = "productLine") String theProductLine, + @OperationParam(name = "additionalData") Bundle theAdditionalData, + @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, + @OperationParam(name = "parameters") Parameters theParameters) { myId = theId; myPeriodStart = thePeriodStart; From c42da79bd6f8a299f719f1a2d55c4062e976e759 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 09:34:21 -0500 Subject: [PATCH 55/75] Spotless. Logging. --- .../java/ca/uhn/fhir/util/ReflectionUtil.java | 3 +- .../rest/server/method/BaseMethodBinding.java | 9 ++++- .../server/method/EmbeddedOperationUtils.java | 4 +- .../fhir/cr/r4/measure/CareGapsParams.java | 28 ++++++-------- .../measure/EvaluateMeasureSingleParams.java | 37 +++++++------------ 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index f78b805e0714..5237bcb7dfc5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -168,7 +168,8 @@ public static Class getGenericCollectionTypeOfMethodParameter(Method theMetho return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]); } - public static Class getGenericCollectionTypeOfConstructorParameter(Constructor theConstructor, Parameter theConstructorParameter) { + public static Class getGenericCollectionTypeOfConstructorParameter( + Constructor theConstructor, Parameter theConstructorParameter) { final int theParamIndex = getIndexOfElement(theConstructor.getParameters(), theConstructorParameter); final Type genericParameterType = theConstructor.getGenericParameterTypes()[theParamIndex]; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index a18e2d7dc6ad..0de1575cdbaf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -254,7 +254,14 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final BaseMethodBindingMethodParameterBuilder baseMethodBindingMethodParameterBuilder = new BaseMethodBindingMethodParameterBuilder(method, theRequest, theMethodParams); - return method.invoke(getProvider(), baseMethodBindingMethodParameterBuilder.build()); + final Object[] outputParams = baseMethodBindingMethodParameterBuilder.build(); + + // LUKETODO: cleanup later + ourLog.info( + "1234: \nmethod: {}, \ninputParams: {}, \noutputParams: {}", + myMethod.getName(), theMethodParams, outputParams); + + return method.invoke(getProvider(), outputParams); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index d610b6032fab..9067fa71b7b0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -152,8 +152,8 @@ static boolean isValidSourceTypeConversion( public static List> getMethodParamsAnnotatedWithEmbeddedOperationParams(Method theMethod) { return Arrays.stream(theMethod.getParameterTypes()) - .filter(EmbeddedOperationUtils::hasEmbeddedOperationParamsAnnotation) - .collect(Collectors.toUnmodifiableList()); + .filter(EmbeddedOperationUtils::hasEmbeddedOperationParamsAnnotation) + .collect(Collectors.toUnmodifiableList()); } private static boolean hasEmbeddedOperationParamsAnnotation(Class theMethodParameterType) { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index cb9610c493cb..c657455ac4db 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -104,24 +104,18 @@ public class CareGapsParams { public CareGapsParams( @OperationParam( - name = "periodStart", - sourceType = String.class, - rangeType = OperationParameterRangeType.START) - ZonedDateTime thePeriodStart, + name = "periodStart", + sourceType = String.class, + rangeType = OperationParameterRangeType.START) + ZonedDateTime thePeriodStart, @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) - ZonedDateTime thePeriodEnd, - @OperationParam(name = "subject") - String theSubject, - @OperationParam(name = "status") - List theStatus, - @OperationParam(name = "measureId") - List theMeasureId, - @OperationParam(name = "measureIdentifier") - List theMeasureIdentifier, - @OperationParam(name = "measureUrl") - List theMeasureUrl, - @OperationParam(name = "nonDocument") - BooleanType theNonDocument) { + ZonedDateTime thePeriodEnd, + @OperationParam(name = "subject") String theSubject, + @OperationParam(name = "status") List theStatus, + @OperationParam(name = "measureId") List theMeasureId, + @OperationParam(name = "measureIdentifier") List theMeasureIdentifier, + @OperationParam(name = "measureUrl") List theMeasureUrl, + @OperationParam(name = "nonDocument") BooleanType theNonDocument) { myPeriodStart = thePeriodStart; myPeriodEnd = thePeriodEnd; mySubject = theSubject; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 6dc8fc885573..f0b32748908c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -97,31 +97,22 @@ public class EvaluateMeasureSingleParams { // LUKETODO: embedded factory constructor annoation // LUKETODO: annotations on constructor parameters instead public EvaluateMeasureSingleParams( - @IdParam - IdType theId, + @IdParam IdType theId, @OperationParam( - name = "periodStart", - sourceType = String.class, - rangeType = OperationParameterRangeType.START) - ZonedDateTime thePeriodStart, + name = "periodStart", + sourceType = String.class, + rangeType = OperationParameterRangeType.START) + ZonedDateTime thePeriodStart, @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) - ZonedDateTime thePeriodEnd, - @OperationParam(name = "reportType") - String theReportType, - @OperationParam(name = "subject") - String theSubject, - @OperationParam(name = "practitioner") - String thePractitioner, - @OperationParam(name = "lastReceivedOn") - String theLastReceivedOn, - @OperationParam(name = "productLine") - String theProductLine, - @OperationParam(name = "additionalData") - Bundle theAdditionalData, - @OperationParam(name = "terminologyEndpoint") - Endpoint theTerminologyEndpoint, - @OperationParam(name = "parameters") - Parameters theParameters) { + ZonedDateTime thePeriodEnd, + @OperationParam(name = "reportType") String theReportType, + @OperationParam(name = "subject") String theSubject, + @OperationParam(name = "practitioner") String thePractitioner, + @OperationParam(name = "lastReceivedOn") String theLastReceivedOn, + @OperationParam(name = "productLine") String theProductLine, + @OperationParam(name = "additionalData") Bundle theAdditionalData, + @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, + @OperationParam(name = "parameters") Parameters theParameters) { myId = theId; myPeriodStart = thePeriodStart; myPeriodEnd = thePeriodEnd; From 92d40ae7b013c64c573c86905f74bc862bd21831 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 10:48:43 -0500 Subject: [PATCH 56/75] Fix new logic for embedded params. No cleanup yet. --- ...seMethodBindingMethodParameterBuilder.java | 50 +++-- .../server/method/EmbeddedOperationUtils.java | 10 +- .../fhir/rest/server/method/MethodUtil.java | 194 +++++++++++++++++- .../server/method/OperationMethodBinding.java | 26 +-- ...thodBindingMethodParameterBuilderTest.java | 5 +- .../EmbeddedParamsInnerClassesAndMethods.java | 93 ++++++++- .../rest/server/method/MethodUtilTest.java | 53 +++-- 7 files changed, 354 insertions(+), 77 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index f14a0b5a3af3..e8e27473daa1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.slf4j.Logger; @@ -99,17 +98,27 @@ private Object[] tryBuildMethodParams() Msg.code(234198927), myMethod, Arrays.toString(myInputMethodParams))); } - final List> parameterTypesWithOperationEmbeddedParam = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - myMethod, EmbeddedOperationParam.class); + ourLog.info( + "1234: START building for method: {}, requestDetails: {}, inputMethodParams: {}", + myMethod.getName(), + myRequestDetails, + Arrays.toString(myInputMethodParams)); + + final List> parameterTypesWithOperationEmbeddedParams = + EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(myMethod); + + // final List> parameterTypesWithOperationEmbeddedParam = + // ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + // myMethod, EmbeddedOperationParams.class); - if (parameterTypesWithOperationEmbeddedParam.size() > 1) { + // if (parameterTypesWithOperationEmbeddedParam.size() > 1) { + if (parameterTypesWithOperationEmbeddedParams.size() > 1) { throw new InternalErrorException(String.format( "%sInvalid operation embedded parameters. More than a single such class is part of method definition: %s", Msg.code(924469634), myMethod.getName())); } - if (parameterTypesWithOperationEmbeddedParam.isEmpty()) { + if (parameterTypesWithOperationEmbeddedParams.isEmpty()) { return myInputMethodParams; } @@ -141,39 +150,50 @@ private Object[] tryBuildMethodParams() Msg.code(924469634), methodName)); } - final Class parameterTypeWithOperationEmbeddedParam = parameterTypesWithOperationEmbeddedParam.get(0); + final Class parameterTypeWithOperationEmbeddedParams = parameterTypesWithOperationEmbeddedParams.get(0); - return determineMethodParamsForOperationEmbeddedParams(parameterTypeWithOperationEmbeddedParam); + return determineMethodParamsForOperationEmbeddedParams(parameterTypeWithOperationEmbeddedParams); } private Object[] determineMethodParamsForOperationEmbeddedParams( - Class theParameterTypeWithOperationEmbeddedParam) + Class theParameterTypeWithOperationEmbeddedParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { final String methodName = myMethod.getName(); ourLog.info( - "1234: invoking parameterTypeWithOperationEmbeddedParam: {} and theMethod: {}", - theParameterTypeWithOperationEmbeddedParam, + "1234: invoking parameterTypeWithOperationEmbeddedParams: {} and theMethod: {}", + theParameterTypeWithOperationEmbeddedParams, methodName); - final Object operationEmbeddedType = buildOperationEmbeddedObject( - methodName, theParameterTypeWithOperationEmbeddedParam, myInputMethodParams); + final Object operationEmbeddedType = + buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParams, myInputMethodParams); ourLog.info( "1234: build method params with embedded object and requestDetails (if applicable) for: {}", operationEmbeddedType); - return buildMethodParamsInCorrectPositions(operationEmbeddedType); + final Object[] params = buildMethodParamsInCorrectPositions(operationEmbeddedType); + + ourLog.info( + "1234: END: method: {}, requestDetails: {}, inputMethodParams: {}, outputMethodParams: {}", + myMethod.getName(), + myRequestDetails, + Arrays.toString(myInputMethodParams), + Arrays.toString(params)); + + return params; } @Nonnull private Object buildOperationEmbeddedObject( - String theMethodName, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) + Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InstantiationException, IllegalAccessException, InvocationTargetException { final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); + // LUKETODO: redo this method in the new constructor param world + // LUKETODO: off by one error final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); final Annotation[] annotations = Arrays.stream(theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 9067fa71b7b0..76a11c026a41 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; @@ -150,13 +150,13 @@ static boolean isValidSourceTypeConversion( && OperationParameterRangeType.NOT_APPLICABLE != theOperationParameterRangeType; } - public static List> getMethodParamsAnnotatedWithEmbeddedOperationParams(Method theMethod) { + public static List> getMethodParamsAnnotatedWithEmbeddableOperationParams(Method theMethod) { return Arrays.stream(theMethod.getParameterTypes()) - .filter(EmbeddedOperationUtils::hasEmbeddedOperationParamsAnnotation) + .filter(EmbeddedOperationUtils::hasEmbeddableOperationParamsAnnotation) .collect(Collectors.toUnmodifiableList()); } - private static boolean hasEmbeddedOperationParamsAnnotation(Class theMethodParameterType) { + private static boolean hasEmbeddableOperationParamsAnnotation(Class theMethodParameterType) { final Annotation[] annotations = theMethodParameterType.getAnnotations(); if (annotations.length == 0) { @@ -169,7 +169,7 @@ private static boolean hasEmbeddedOperationParamsAnnotation(Class theMethodPa Msg.code(9132164), theMethodParameterType)); } - return EmbeddedOperationParams.class == annotations[0].annotationType(); + return EmbeddableOperationParams.class == annotations[0].annotationType(); } private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 0be544334fda..fdbd3d47533d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -31,7 +31,7 @@ import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Elements; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; @@ -71,12 +71,16 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -350,9 +354,117 @@ public static List getResourceParameters( parameterType = newParameterType; } } else if (nextAnnotation instanceof EmbeddedOperationParams) { - final List> operationEmbeddedTypes = - ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - methodToUse, EmbeddedOperationParam.class); + // LUKETODO: cleanup + // NEW + if (op == null) { + throw new ConfigurationException(Msg.code(846192641) + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + + methodToUse.toGenericString()); + } + + final List> embeddedParamsClasses = Arrays.stream(methodToUse.getParameterTypes()) + .filter(paramType -> paramType.isAnnotationPresent(EmbeddableOperationParams.class)) + .collect(Collectors.toUnmodifiableList()); + + // LUKETODO; better error? + if (embeddedParamsClasses.isEmpty()) { + throw new ConfigurationException(String.format( + "%sThere is no param with @EmbeddableOperationParams is supported for now for method: %s", + Msg.code(9999924), methodToUse.getName())); + } + + // LUKETODO; better error? + if (embeddedParamsClasses.size() > 1) { + throw new ConfigurationException(String.format( + "%sMore than one param with with @EmbeddableOperationParams for method: %s", + Msg.code(9999927), methodToUse.getName())); + } + + final Class soleEmbeddedParamClass = embeddedParamsClasses.get(0); + + final Constructor[] constructorsForEmbeddableOperationParams = + soleEmbeddedParamClass.getConstructors(); + + // LUKETODO; better error? + if (constructorsForEmbeddableOperationParams.length == 0) { + throw new ConfigurationException(String.format( + "%sThere is no constructor with @EmbeddableOperationParams is supported for now for method: %s", + Msg.code(9999924), methodToUse.getName())); + } + + // LUKETODO; better error? + if (constructorsForEmbeddableOperationParams.length > 1) { + throw new ConfigurationException(String.format( + "%sOnly one constructor with @EmbeddableOperationParams is supported but there is mulitple for method: %s", + Msg.code(9999927), methodToUse.getName())); + } + + final Constructor constructor = constructorsForEmbeddableOperationParams[0]; + + final Parameter[] constructorParams = constructor.getParameters(); + + for (Parameter constructorParam : constructorParams) { + final Annotation[] annotations = constructorParam.getAnnotations(); + + // LUKETODO: test + if (annotations.length == 0) { + throw new ConfigurationException(String.format( + "%s Constructor params have no annotation for embedded params class: %s and method: %s", + Msg.code(9999937), + constructor.getDeclaringClass().getName(), + methodToUse.getName())); + } + + // LUKETODO: test + if (annotations.length > 1) { + throw new ConfigurationException(String.format( + "%s Constructor params have more than one annotation for embedded params: %s and method: %s", + Msg.code(9999947), + constructor.getDeclaringClass().getName(), + methodToUse.getName())); + } + + final Annotation soleAnnotation = annotations[0]; + + IParameter innerParam = null; + if (soleAnnotation instanceof IdParam) { + // LUKETODO: we're missing this parameter when we build the method binding + innerParam = new NullParameter(); + // Need to add this explicitly + parameters.add(innerParam); + } else if (soleAnnotation instanceof OperationParam) { + final OperationParam operationParam = (OperationParam) soleAnnotation; + final String description = ParametersUtil.extractDescription(annotations); + final List examples = ParametersUtil.extractExamples(annotations); + final OperationParameter operationParameter = new OperationParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples, + operationParam.sourceType(), + operationParam.rangeType()); + + final ParamInitializationContext paramContext = + buildParamContext(constructor, constructorParam, operationParameter); + + innerParam = operationParameter; + paramContexts.add(paramContext); + } + + param = innerParam; + } + + // LUKETODO: need to disable the below code + + // OLD + // final List> operationEmbeddedTypes = + // ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( + // methodToUse, EmbeddedOperationParam.class); + + final List> operationEmbeddedTypes = List.of(); if (op == null) { throw new ConfigurationException(Msg.code(846192641) @@ -468,12 +580,6 @@ public Object outgoingClient(Object theObject) { } } - if (paramContexts.isEmpty() || !(param instanceof EmbeddedOperationParameter)) { - // RequestDetails if it's last - paramContexts.add( - new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); - } - if (param == null) { throw new ConfigurationException( Msg.code(408) + "Parameter #" + (paramIndex + 1) + "/" + (parameterTypes.length) @@ -482,6 +588,21 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } + // LUKETODO: refactor into some sort of static method + // LUKETODO: comment that this is a guard against adding the entire embeddable parameter as an + // OperationParameter + // LUKETODO: find a better guard for this? + + // LUKETODO: this doesn't work because the contexts are not empty + + if (paramContexts.isEmpty() + || Arrays.stream(parameterType.getAnnotations()) + .noneMatch(annotation -> annotation instanceof EmbeddableOperationParams)) { + // RequestDetails if it's last + paramContexts.add( + new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); + } + for (ParamInitializationContext paramContext : paramContexts) { paramContext.initialize(methodToUse); parameters.add(paramContext.getParam()); @@ -491,4 +612,57 @@ public Object outgoingClient(Object theObject) { } return parameters; } + + private static ParamInitializationContext buildParamContext( + Constructor theConstructor, Parameter theConstructorParameter, OperationParameter theOperationParam) { + + final Class genericParameter = + ReflectionUtil.getGenericCollectionTypeOfConstructorParameter(theConstructor, theConstructorParameter); + + Class parameterType = theConstructorParameter.getType(); + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + // Flat collection + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = unsafeCast(parameterType); + parameterType = genericParameter; + if (parameterType == null) { + final String error = String.format( + "%s Cannot find generic type for field: %s in class: %s for constructor: %s", + Msg.code(724612469), + theConstructorParameter.getName(), + theConstructorParameter.getClass().getCanonicalName(), + theConstructor.getName()); + throw new ConfigurationException(error); + } + + // Collection of a Collection: Permitted + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = unsafeCast(parameterType); + } + + // Collection of a Collection of a Collection: Prohibited + if (Collection.class.isAssignableFrom(parameterType)) { + final String error = String.format( + "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for constructor: %s", + Msg.code(724612469), + theConstructorParameter.getName(), + theConstructorParameter.getClass().getCanonicalName(), + theConstructor.getName()); + throw new ConfigurationException(error); + } + } + + // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later + + return new ParamInitializationContext( + theOperationParam, parameterType, outerCollectionType, innerCollectionType); + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T) theObject; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index a7556b48da2c..aa872e982352 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -57,7 +57,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -363,7 +362,7 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques throws BaseServerResponseException, IOException { if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) { IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null); - theRequest.getUserData().put(determineUserDataKey(getMethod()), requestContents); + theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents); } return super.invokeServer(theServer, theRequest); } @@ -439,7 +438,8 @@ public boolean isDeleteEnabled() { @Override protected void populateRequestDetailsForInterceptor(RequestDetails theRequestDetails, Object[] theMethodParams) { super.populateRequestDetailsForInterceptor(theRequestDetails, theMethodParams); - IBaseResource resource = getBaseResource(theRequestDetails); + IBaseResource resource = + (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); theRequestDetails.setResource(resource); } @@ -532,26 +532,6 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( return OperationIdParamDetails.EMPTY; } - private IBaseResource getBaseResource(RequestDetails theRequestDetails) { - final Map userData = theRequestDetails.getUserData(); - - if (userData.containsKey(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY)) { - return (IBaseResource) userData.get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); - } - - if (userData.containsKey(EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY)) { - return (IBaseResource) userData.get(EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY); - } - - return null; - } - - private static String determineUserDataKey(Method theMethod) { - return EmbeddedOperationUtils.hasAnyMethodParamsWithClassesWithFieldsWithEmbeddedOperationParams(theMethod) - ? EmbeddedOperationParameter.REQUEST_CONTENTS_USERDATA_KEY - : OperationParameter.REQUEST_CONTENTS_USERDATA_KEY; - } - public static class ReturnType { private int myMax; private int myMin; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index b2050ca3e815..63e4ee095d08 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -21,6 +21,7 @@ import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithoutAnnotations; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; @@ -70,7 +71,7 @@ void happyPathOperationParamsNonEmptyParams() { @Test void happyPathOperationEmbeddedTypesNoRequestDetails() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; @@ -81,7 +82,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { @Test void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{null, null}; final Object[] expectedOutputParams = new Object[]{new SampleParams(null, null)}; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java index 14e59c254de3..83a575cc2f17 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -47,7 +47,7 @@ class EmbeddedParamsInnerClassesAndMethods { static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; static final String SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE = "sampleMethodParamNoEmbeddedType"; static final String SIMPLE_METHOD_WITH_PARAMS_CONVERSION = "simpleMethodWithParamsConversion"; - + static final String SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION = "sampleMethodEmbeddedTypeIdTypeAndTypeConversion"; static final String EXPAND = "expand"; static final String OP_INSTANCE_OR_TYPE = "opInstanceOrType"; @@ -163,7 +163,11 @@ static class SampleParams { @EmbeddedOperationParam(name = "param2") private final List myParam2; - public SampleParams(String myParam1, List myParam2) { + public SampleParams( + @OperationParam(name = "param1") + String myParam1, + @OperationParam(name = "param2") + List myParam2) { this.myParam1 = myParam1; this.myParam2 = myParam2; } @@ -206,9 +210,13 @@ static class ParamsWithTypeConversion { @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; - public ParamsWithTypeConversion(ZonedDateTime myPeriodStart, ZonedDateTime myPeriodEnd) { - this.myPeriodStart = myPeriodStart; - this.myPeriodEnd = myPeriodEnd; + public ParamsWithTypeConversion( + @OperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) + ZonedDateTime thePeriodStart, + @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) + ZonedDateTime thePeriodEnd) { + myPeriodStart = thePeriodStart; + myPeriodEnd = thePeriodEnd; } public ZonedDateTime getPeriodStart() { @@ -256,7 +264,15 @@ static class SampleParamsWithIdParam { @EmbeddedOperationParam(name = "param3") private final BooleanType myParam3; - public SampleParamsWithIdParam(IdType theId, String theParam1, List theParam2, BooleanType theParam3) { + public SampleParamsWithIdParam( + @IdParam + IdType theId, + @OperationParam(name = "param1") + String theParam1, + @OperationParam(name = "param2") + List theParam2, + @OperationParam(name = "param3") + BooleanType theParam3) { myId = theId; myParam1 = theParam1; myParam2 = theParam2; @@ -305,6 +321,65 @@ public String toString() { } } + @EmbeddableOperationParams + static class ParamsWithIdParamAndTypeConversion { + @IdParam + private final IdType myId; + + @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) + private final ZonedDateTime myPeriodStart; + + @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) + private final ZonedDateTime myPeriodEnd; + + public ParamsWithIdParamAndTypeConversion( + @IdParam + IdType theId, + @OperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) + ZonedDateTime thePeriodStart, + @OperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) + ZonedDateTime thePeriodEnd) { + myId = theId; + myPeriodStart = thePeriodStart; + myPeriodEnd = thePeriodEnd; + } + + public IdType getId() { + return myId; + } + + public ZonedDateTime getPeriodStart() { + return myPeriodStart; + } + + public ZonedDateTime getPeriodEnd() { + return myPeriodEnd; + } + + @Override + public boolean equals(Object theO) { + if (theO == null || getClass() != theO.getClass()) { + return false; + } + ParamsWithIdParamAndTypeConversion that = (ParamsWithIdParamAndTypeConversion) theO; + return Objects.equals(myId, that.myId) && Objects.equals(myPeriodStart, that.myPeriodStart) && Objects.equals(myPeriodEnd, that.myPeriodEnd); + } + + @Override + public int hashCode() { + return Objects.hash(myId, myPeriodStart, myPeriodEnd); + } + + @Override + public String toString() { + return new StringJoiner(", ", ParamsWithIdParamAndTypeConversion.class.getSimpleName() + "[", "]") + .add("myId=" + myId) + .add("myPeriodStart=" + myPeriodStart) + .add("myPeriodEnd=" + myPeriodEnd) + .toString(); + } + } + @Operation(name="sampleMethodEmbeddedTypeRequestDetailsFirst") String sampleMethodEmbeddedTypeRequestDetailsFirst(RequestDetails theRequestDetails, @EmbeddedOperationParams SampleParams theParams) { // return something arbitrary @@ -344,6 +419,12 @@ String sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType(RequestDetails theR return theRequestDetails.getId().getValue() + theParams.getParam1(); } + @Operation(name="sampleMethodEmbeddedTypeIdTypeAndTypeConversion") + String sampleMethodEmbeddedTypeIdTypeAndTypeConversion(@EmbeddedOperationParams ParamsWithIdParamAndTypeConversion theParams) { + // return something arbitrary + return theParams.getId().getValue(); + } + String sampleMethodEmbeddedTypeRequestDetailsLastWithIdType(@EmbeddedOperationParams SampleParamsWithIdParam theParams, RequestDetails theRequestDetails) { // return something arbitrary return theRequestDetails.getId().getValue() + theParams.getParam1(); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 6217acae5438..66cc1216e843 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithIdParamAndTypeConversion; import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; @@ -19,6 +20,7 @@ import java.util.List; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; @@ -106,11 +108,11 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -124,12 +126,12 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -143,11 +145,11 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class, RequestDetailsParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), new RequestDetailsParameterToAssert() ); @@ -162,13 +164,13 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new EmbeddedOperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -182,11 +184,30 @@ void paramsConversionZonedDateTime() { assertThat(resourceParameters).isNotNull(); assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(EmbeddedOperationParameter.class, EmbeddedOperationParameter.class); + assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( - new EmbeddedOperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), - new EmbeddedOperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) + new OperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), + new OperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); + } + + @Test + void paramsConversionIdTypeZonedDateTime() { + final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION, ParamsWithIdParamAndTypeConversion.class); + + assertThat(resourceParameters).isNotNull(); + assertThat(resourceParameters).isNotEmpty(); + assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), + new OperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) ); assertThat(resourceParameters) From 3161bb56b2e9c44b52977689fc745b995485d531 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 13:35:06 -0500 Subject: [PATCH 57/75] Move closer to remove the old embedded params functionality. Totally migrate caregaps and evaluate measure(s) params. Still messy. Disable enforcer in pom. --- ...seMethodBindingMethodParameterBuilder.java | 61 ++--- .../server/method/EmbeddedOperationUtils.java | 6 +- .../method/EmbeddedParameterConverter.java | 250 +++++++++--------- .../fhir/rest/server/method/MethodUtil.java | 74 ++---- .../server/method/OperationMethodBinding.java | 29 +- .../fhir/cr/r4/measure/CareGapsParams.java | 12 - .../measure/EvaluateMeasureSingleParams.java | 15 -- .../EmbeddedParamsInnerClassesAndMethods.java | 11 - pom.xml | 3 + 9 files changed, 191 insertions(+), 270 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index e8e27473daa1..694f3c0ecef2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -32,7 +33,6 @@ import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; -import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -47,6 +47,7 @@ import static java.util.function.Predicate.not; +// LUKETODO: redo javadoc /** * Responsible for either passing to objects params straight through to the method call or converting them to * fit within a class that has fields annotated with {@link EmbeddedOperationParam} and to also handle placement @@ -107,11 +108,6 @@ private Object[] tryBuildMethodParams() final List> parameterTypesWithOperationEmbeddedParams = EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(myMethod); - // final List> parameterTypesWithOperationEmbeddedParam = - // ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - // myMethod, EmbeddedOperationParams.class); - - // if (parameterTypesWithOperationEmbeddedParam.size() > 1) { if (parameterTypesWithOperationEmbeddedParams.size() > 1) { throw new InternalErrorException(String.format( "%sInvalid operation embedded parameters. More than a single such class is part of method definition: %s", @@ -196,36 +192,14 @@ private Object buildOperationEmbeddedObject( // LUKETODO: off by one error final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); - final Annotation[] annotations = Arrays.stream(theParameterTypeWithOperationEmbeddedParam.getDeclaredFields()) - .map(AccessibleObject::getAnnotations) - .filter(array -> array.length == 1) - .flatMap(Arrays::stream) - .toArray(Annotation[]::new); - - if (methodParamsWithoutRequestDetails.length != constructor.getParameterCount()) { - final String error = String.format( - "%smismatch between constructor args: %s and non-request details parameter args: %s", - Msg.code(475326592), - Arrays.toString(constructor.getParameterTypes()), - Arrays.toString(methodParamsWithoutRequestDetails)); - throw new InternalErrorException(error); - } - - if (methodParamsWithoutRequestDetails.length != annotations.length) { - final String error = String.format( - "%smismatch between non-request details parameter args: %s and number of annotations: %s", - Msg.code(475326593), - Arrays.toString(methodParamsWithoutRequestDetails), - Arrays.toString(annotations)); - throw new InternalErrorException(error); - } + final Annotation[] constructorAnnotations = constructor.getAnnotations(); final Parameter[] constructorParameters = validateAndGetConstructorParameters(constructor); - validMethodParamTypes(methodParamsWithoutRequestDetails, constructorParameters, annotations); + validMethodParamTypes(methodParamsWithoutRequestDetails, constructorParameters); final Object[] convertedParams = - convertParamsIfNeeded(methodParamsWithoutRequestDetails, constructorParameters, annotations); + convertParamsIfNeeded(methodParamsWithoutRequestDetails, constructorParameters, constructorAnnotations); return constructor.newInstance(convertedParams); } @@ -235,8 +209,10 @@ private Object[] convertParamsIfNeeded( Parameter[] theConstructorParameters, Annotation[] theAnnotations) { + final Annotation[] annotations = Arrays.stream(theConstructorParameters).map(Parameter::getAnnotations).filter(array -> array.length == 1).map(array -> array[0]).toArray(Annotation[]::new); + if (!EmbeddedOperationUtils.hasAnyValidSourceTypeConversions( - theMethodParamsWithoutRequestDetails, theConstructorParameters, theAnnotations)) { + theMethodParamsWithoutRequestDetails, theConstructorParameters, annotations)) { return theMethodParamsWithoutRequestDetails; } @@ -254,19 +230,20 @@ private Object convertParamIfNeeded( int theIndex) { final Object paramAtIndex = theMethodParamsWithoutRequestDetails[theIndex]; - final Annotation annotation = theAnnotations[theIndex]; + final Annotation annotation = theConstructorParameters[theIndex].getAnnotations()[0]; +// final Annotation annotation = theAnnotations[theIndex]; if (paramAtIndex == null) { return paramAtIndex; } - if (!(annotation instanceof EmbeddedOperationParam)) { + if (!(annotation instanceof OperationParam)) { return paramAtIndex; } - final EmbeddedOperationParam embeddedParamAtIndex = (EmbeddedOperationParam) annotation; + final OperationParam operationParamAtIndex = (OperationParam) annotation; final Class paramClassAtIndex = paramAtIndex.getClass(); - final OperationParameterRangeType rangeType = embeddedParamAtIndex.rangeType(); + final OperationParameterRangeType rangeType = operationParamAtIndex.rangeType(); final Parameter constructorParameter = theConstructorParameters[theIndex]; final Class constructorParameterType = constructorParameter.getType(); @@ -336,8 +313,7 @@ private Object[] buildMethodParamsInCorrectPositions(Object operationEmbeddedTyp private void validMethodParamTypes( Object[] theMethodParamsWithoutRequestDetails, - Parameter[] theConstructorParameters, - Annotation[] theAnnotations) { + Parameter[] theConstructorParameters) { if (theMethodParamsWithoutRequestDetails.length != theConstructorParameters.length) { final String error = String.format( @@ -353,7 +329,8 @@ private void validMethodParamTypes( validateMethodParamType( theMethodParamsWithoutRequestDetails[index], theConstructorParameters[index].getType(), - theAnnotations[index]); + // LUKETODO: fix by not passing this directly + theConstructorParameters[index].getAnnotations()[0]); } } @@ -366,9 +343,9 @@ private void validateMethodParamType(Object theMethodParam, Class theParamete final Class methodParamClass = theMethodParam.getClass(); - final Optional optOperationEmbeddedParam = - theAnnotation instanceof EmbeddedOperationParam - ? Optional.of((EmbeddedOperationParam) theAnnotation) + final Optional optOperationEmbeddedParam = + theAnnotation instanceof OperationParam + ? Optional.of((OperationParam) theAnnotation) : Optional.empty(); optOperationEmbeddedParam.ifPresent(embeddedParam -> { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 76a11c026a41..089b8023d9dd 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; @@ -49,6 +50,7 @@ public class EmbeddedOperationUtils { private EmbeddedOperationUtils() {} + // LUKETODO: redo for OperationParam /** * Validate that a constructor for a class with fields that are {@link EmbeddedOperationParam} declares its * parameters in the same order as the fields are declared in the class. It also validates that the fields are @@ -120,8 +122,8 @@ private static Boolean isValidSourceTypeConversion( final Class constructorParamType = theConstructorParameters[theIndex].getType(); final Annotation annotation = theAnnotations[theIndex]; - if (annotation instanceof EmbeddedOperationParam) { - final EmbeddedOperationParam embeddedOperationParam = (EmbeddedOperationParam) annotation; + if (annotation instanceof OperationParam) { + final OperationParam embeddedOperationParam = (OperationParam) annotation; final OperationParameterRangeType operationParameterRangeType = embeddedOperationParam.rangeType(); if (isValidSourceTypeConversion(methodParamClass, constructorParamType, operationParameterRangeType)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index d867ee9eafdb..a54d4c129c4e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -39,136 +39,138 @@ import static org.slf4j.LoggerFactory.*; +// LUKETODO: redo javadoc +// LUKETODO: convert to new design /** * Leveraged by {@link MethodUtil} exclusively to convert {@link EmbeddedOperationParam} parameters for a method to * either a {@link NullParameter} or an {@link EmbeddedOperationParam}. */ public class EmbeddedParameterConverter { private static final org.slf4j.Logger ourLog = getLogger(EmbeddedParameterConverter.class); - - private final FhirContext myContext; - private final Method myMethod; - private final Operation myOperation; - private final Class myOperationEmbeddedType; - - public EmbeddedParameterConverter( - FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { - myContext = theContext; - myMethod = theMethod; - myOperation = theOperation; - myOperationEmbeddedType = theOperationEmbeddedType; - } - - List convert() { - return Arrays.stream(validateConstructorArgsAndReturnFields()) - .map(this::convertField) - .collect(Collectors.toUnmodifiableList()); - } - - private Field[] validateConstructorArgsAndReturnFields() { - EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); - - return myOperationEmbeddedType.getDeclaredFields(); - } - - private EmbeddedParameterConverterContext convertField(Field theField) { - final String fieldName = theField.getName(); - final Class fieldType = theField.getType(); - final Annotation[] fieldAnnotations = theField.getAnnotations(); - - if (fieldAnnotations.length < 1) { - throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); - } - - if (fieldAnnotations.length > 1) { - throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(999998), fieldName, myMethod.getName())); - } - - final Annotation fieldAnnotation = fieldAnnotations[0]; - - if (fieldAnnotation instanceof IdParam) { - return EmbeddedParameterConverterContext.forParameter(new NullParameter()); - } else if (fieldAnnotation instanceof EmbeddedOperationParam) { - final ParamInitializationContext paramContext = - buildParamContext(fieldType, theField, (EmbeddedOperationParam) fieldAnnotation); - - return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); - } else { - final String error = String.format( - "%sUnsupported annotation type: %s for a class: %s with OperationEmbeddedParams which is part of method: %s: ", - Msg.code(912732197), myOperationEmbeddedType, fieldAnnotation.annotationType(), myMethod.getName()); - - throw new ConfigurationException(error); - } - } - - private ParamInitializationContext buildParamContext( - Class theFieldType, Field theField, EmbeddedOperationParam theEmbeddedOperationParam) { - - final EmbeddedOperationParameter embeddedOperationParameter = - getOperationEmbeddedParameter(theEmbeddedOperationParam); - - Class parameterType = theFieldType; - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - // Flat collection - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = unsafeCast(parameterType); - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - if (parameterType == null) { - final String error = String.format( - "%s Cannot find generic type for field: %s in class: %s for method: %s", - Msg.code(724612469), - theField.getName(), - theField.getDeclaringClass().getCanonicalName(), - myMethod.getName()); - throw new ConfigurationException(error); - } - - // Collection of a Collection: Permitted - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = unsafeCast(parameterType); - parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); - } - - // Collection of a Collection of a Collection: Prohibited - if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { - final String error = String.format( - "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for method: %s", - Msg.code(724612469), - theField.getName(), - theField.getDeclaringClass().getCanonicalName(), - myMethod.getName()); - throw new ConfigurationException(error); - } - } - - // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later - - return new ParamInitializationContext( - embeddedOperationParameter, parameterType, outerCollectionType, innerCollectionType); - } - - @Nonnull - private EmbeddedOperationParameter getOperationEmbeddedParameter(EmbeddedOperationParam operationParam) { - final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; - - return new EmbeddedOperationParameter( - myContext, - myOperation.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - ParametersUtil.extractDescription(fieldAnnotationArray), - ParametersUtil.extractExamples(fieldAnnotationArray), - operationParam.sourceType(), - operationParam.rangeType()); - } +// +// private final FhirContext myContext; +// private final Method myMethod; +// private final Operation myOperation; +// private final Class myOperationEmbeddedType; +// +// public EmbeddedParameterConverter( +// FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { +// myContext = theContext; +// myMethod = theMethod; +// myOperation = theOperation; +// myOperationEmbeddedType = theOperationEmbeddedType; +// } +// +// List convert() { +// return Arrays.stream(validateConstructorArgsAndReturnFields()) +// .map(this::convertField) +// .collect(Collectors.toUnmodifiableList()); +// } +// +// private Field[] validateConstructorArgsAndReturnFields() { +// EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); +// +// return myOperationEmbeddedType.getDeclaredFields(); +// } +// +// private EmbeddedParameterConverterContext convertField(Field theField) { +// final String fieldName = theField.getName(); +// final Class fieldType = theField.getType(); +// final Annotation[] fieldAnnotations = theField.getAnnotations(); +// +// if (fieldAnnotations.length < 1) { +// throw new ConfigurationException(String.format( +// "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); +// } +// +// if (fieldAnnotations.length > 1) { +// throw new ConfigurationException(String.format( +// "%sMore than one annotation for field: %s for method: %s", +// Msg.code(999998), fieldName, myMethod.getName())); +// } +// +// final Annotation fieldAnnotation = fieldAnnotations[0]; +// +// if (fieldAnnotation instanceof IdParam) { +// return EmbeddedParameterConverterContext.forParameter(new NullParameter()); +// } else if (fieldAnnotation instanceof EmbeddedOperationParam) { +// final ParamInitializationContext paramContext = +// buildParamContext(fieldType, theField, (EmbeddedOperationParam) fieldAnnotation); +// +// return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); +// } else { +// final String error = String.format( +// "%sUnsupported annotation type: %s for a class: %s with OperationEmbeddedParams which is part of method: %s: ", +// Msg.code(912732197), myOperationEmbeddedType, fieldAnnotation.annotationType(), myMethod.getName()); +// +// throw new ConfigurationException(error); +// } +// } +// +// private ParamInitializationContext buildParamContext( +// Class theFieldType, Field theField, EmbeddedOperationParam theEmbeddedOperationParam) { +// +// final EmbeddedOperationParameter embeddedOperationParameter = +// getOperationEmbeddedParameter(theEmbeddedOperationParam); +// +// Class parameterType = theFieldType; +// Class> outerCollectionType = null; +// Class> innerCollectionType = null; +// +// // Flat collection +// if (Collection.class.isAssignableFrom(parameterType)) { +// innerCollectionType = unsafeCast(parameterType); +// parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); +// if (parameterType == null) { +// final String error = String.format( +// "%s Cannot find generic type for field: %s in class: %s for method: %s", +// Msg.code(724612469), +// theField.getName(), +// theField.getDeclaringClass().getCanonicalName(), +// myMethod.getName()); +// throw new ConfigurationException(error); +// } +// +// // Collection of a Collection: Permitted +// if (Collection.class.isAssignableFrom(parameterType)) { +// outerCollectionType = innerCollectionType; +// innerCollectionType = unsafeCast(parameterType); +// parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); +// } +// +// // Collection of a Collection of a Collection: Prohibited +// if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { +// final String error = String.format( +// "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for method: %s", +// Msg.code(724612469), +// theField.getName(), +// theField.getDeclaringClass().getCanonicalName(), +// myMethod.getName()); +// throw new ConfigurationException(error); +// } +// } +// +// // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later +// +// return new ParamInitializationContext( +// embeddedOperationParameter, parameterType, outerCollectionType, innerCollectionType); +// } +// +// @Nonnull +// private EmbeddedOperationParameter getOperationEmbeddedParameter(EmbeddedOperationParam operationParam) { +// final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; +// +// return new EmbeddedOperationParameter( +// myContext, +// myOperation.name(), +// operationParam.name(), +// operationParam.min(), +// operationParam.max(), +// ParametersUtil.extractDescription(fieldAnnotationArray), +// ParametersUtil.extractExamples(fieldAnnotationArray), +// operationParam.sourceType(), +// operationParam.rangeType()); +// } @SuppressWarnings("unchecked") private static T unsafeCast(Object theObject) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index fdbd3d47533d..3b8cb389c313 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -457,56 +457,30 @@ public static List getResourceParameters( param = innerParam; } - // LUKETODO: need to disable the below code - - // OLD - // final List> operationEmbeddedTypes = - // ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - // methodToUse, EmbeddedOperationParam.class); - - final List> operationEmbeddedTypes = List.of(); - - if (op == null) { - throw new ConfigurationException(Msg.code(846192641) - + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " - + methodToUse.toGenericString()); - } - - if (operationEmbeddedTypes.size() > 1) { - throw new ConfigurationException(String.format( - "%sOnly one type with embedded params is supported for now for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - - if (!operationEmbeddedTypes.isEmpty()) { - final EmbeddedParameterConverter embeddedParameterConverter = - new EmbeddedParameterConverter( - theContext, theMethod, op, operationEmbeddedTypes.get(0)); - - final List outerContexts = - embeddedParameterConverter.convert(); - - for (EmbeddedParameterConverterContext outerContext : outerContexts) { - if (outerContext.getParameter() != null) { - parameters.add(outerContext.getParameter()); - } - final ParamInitializationContext paramContext = outerContext.getParamContext(); - - if (paramContext != null) { - paramContexts.add(paramContext); - - // N.B. This a hack used only to pass the null check below, which is crucial to the - // non-embedded params logic - param = paramContext.getParam(); - } - } - } else { - // More than likely this will result in the param == null Exception below - ourLog.warn( - "Method '{}' has no parameters with annotations. Don't know how to handle this parameter", - methodToUse.getName()); - } - ourLog.info("1234: NEW CODE PATH!!!!!"); + // LUKETODO: extract the pattent from the code below then delete: +// if (!operationEmbeddedTypes.isEmpty()) { +// final EmbeddedParameterConverter embeddedParameterConverter = +// new EmbeddedParameterConverter( +// theContext, theMethod, op, operationEmbeddedTypes.get(0)); +// +// final List outerContexts = +// embeddedParameterConverter.convert(); +// +// for (EmbeddedParameterConverterContext outerContext : outerContexts) { +// if (outerContext.getParameter() != null) { +// parameters.add(outerContext.getParameter()); +// } +// final ParamInitializationContext paramContext = outerContext.getParamContext(); +// +// if (paramContext != null) { +// paramContexts.add(paramContext); +// +// // N.B. This a hack used only to pass the null check below, which is crucial to the +// // non-embedded params logic +// param = paramContext.getParam(); +// } +// } +// } } else if (nextAnnotation instanceof Validate.Mode) { if (!parameterType.equals(ValidationModeEnum.class)) { throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index aa872e982352..caa37af4843b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -40,7 +39,6 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.ParametersUtil; -import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -50,9 +48,10 @@ import java.io.IOException; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -452,8 +451,7 @@ public String getCanonicalUrl() { } private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { - final List> operationEmbeddedTypes = ReflectionUtil.getMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, EmbeddedOperationParam.class); + final List> operationEmbeddedTypes = EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(theMethod); if (!operationEmbeddedTypes.isEmpty()) { return findIdParamIndexForTypeWithEmbeddedParams(theMethod, operationEmbeddedTypes, theContext); @@ -497,25 +495,28 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( typeWithEmbeddedParams, RequestDetails.class, SystemRequestDetails.class)) { // skip } else { - final Field[] fields = typeWithEmbeddedParams.getDeclaredFields(); + // LUKETODO: fix + final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(typeWithEmbeddedParams); + + final Parameter[] constructorParams = constructor.getParameters(); int paramIndex = 0; - for (Field field : fields) { - ParameterUtil.validateIdType(theMethod, theContext, field.getType()); + for (Parameter constructorParam : constructorParams) { + ParameterUtil.validateIdType(theMethod, theContext, constructorParam.getType()); - final String fieldName = field.getName(); - final Annotation[] fieldAnnotations = field.getAnnotations(); + final String constructorParamName = constructorParam.getName(); + final Annotation[] fieldAnnotations = constructorParam.getAnnotations(); if (fieldAnnotations.length < 1) { throw new ConfigurationException(String.format( - "%sNo annotations for field: %s for method: %s", - Msg.code(126362643), fieldName, theMethod.getName())); + "%sNo annotations for constructor param: %s for method: %s", + Msg.code(126362643), constructorParamName, theMethod.getName())); } if (fieldAnnotations.length > 1) { throw new ConfigurationException(String.format( - "%sMore than one annotation for field: %s for method: %s", - Msg.code(195614846), fieldName, theMethod.getName())); + "%sMore than one annotation for constructor param: %s for method: %s", + Msg.code(195614846), constructorParamName, theMethod.getName())); } final Annotation fieldAnnotation = fieldAnnotations[0]; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index c657455ac4db..3c0ddcca24d4 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -74,32 +74,20 @@ */ @EmbeddableOperationParams public class CareGapsParams { - // LUKETODO: when ready, cut the umbilical cord - @EmbeddedOperationParam( - name = "periodStart", - sourceType = String.class, - rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; - @EmbeddedOperationParam(name = "subject") private final String mySubject; - @EmbeddedOperationParam(name = "status") private final List myStatus; - @EmbeddedOperationParam(name = "measureId") private final List myMeasureId; - @EmbeddedOperationParam(name = "measureIdentifier") private final List myMeasureIdentifier; - @EmbeddedOperationParam(name = "measureUrl") private final List myMeasureUrl; - @EmbeddedOperationParam(name = "nonDocument") private final BooleanType myNonDocument; public CareGapsParams( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index f0b32748908c..e51dfe0a9d87 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -57,41 +57,26 @@ // LUKETODO: make code use or at least validate this annotation @EmbeddableOperationParams public class EvaluateMeasureSingleParams { - @IdParam private final IdType myId; - // LUKETODO: OperationParam - @EmbeddedOperationParam( - name = "periodStart", - sourceType = String.class, - rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; - @EmbeddedOperationParam(name = "reportType") private final String myReportType; - @EmbeddedOperationParam(name = "subject") private final String mySubject; - @EmbeddedOperationParam(name = "practitioner") private final String myPractitioner; - @EmbeddedOperationParam(name = "lastReceivedOn") private final String myLastReceivedOn; - @EmbeddedOperationParam(name = "productLine") private final String myProductLine; - @EmbeddedOperationParam(name = "additionalData") private final Bundle myAdditionalData; - @EmbeddedOperationParam(name = "terminologyEndpoint") private final Endpoint myTerminologyEndpoint; - @EmbeddedOperationParam(name = "parameters") private final Parameters myParameters; // LUKETODO: embedded factory constructor annoation diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java index 83a575cc2f17..2226c6a3f414 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -5,7 +5,6 @@ import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -157,10 +156,8 @@ public String toString() { // Ignore warnings that these classes can be records. Converting them to records will make the tests fail @EmbeddableOperationParams static class SampleParams { - @EmbeddedOperationParam(name = "param1") private final String myParam1; - @EmbeddedOperationParam(name = "param2") private final List myParam2; public SampleParams( @@ -204,10 +201,8 @@ public String toString() { @EmbeddableOperationParams static class ParamsWithTypeConversion { - @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; public ParamsWithTypeConversion( @@ -252,16 +247,12 @@ public String toString() { // Ignore warnings that these classes can be records. Converting them to records will make the tests fail @EmbeddableOperationParams static class SampleParamsWithIdParam { - @IdParam private final IdType myId; - @EmbeddedOperationParam(name = "param1") private final String myParam1; - @EmbeddedOperationParam(name = "param2") private final List myParam2; - @EmbeddedOperationParam(name = "param3") private final BooleanType myParam3; public SampleParamsWithIdParam( @@ -326,10 +317,8 @@ static class ParamsWithIdParamAndTypeConversion { @IdParam private final IdType myId; - @EmbeddedOperationParam(name = "periodStart", sourceType = String.class, rangeType = OperationParameterRangeType.START) private final ZonedDateTime myPeriodStart; - @EmbeddedOperationParam(name = "periodEnd", sourceType = String.class, rangeType = OperationParameterRangeType.END) private final ZonedDateTime myPeriodEnd; public ParamsWithIdParamAndTypeConversion( diff --git a/pom.xml b/pom.xml index d717ab5e57f3..a18a479cdcb7 100644 --- a/pom.xml +++ b/pom.xml @@ -1111,6 +1111,9 @@ 3.5.0 3.6.0 + + true + From a685744369c97bd9ddff1d1fe841157eb1dac024 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 14:37:18 -0500 Subject: [PATCH 58/75] Delete EmbeddedOperationParam.java and EmbeddedOperationParameter.java. Redo javadoc and methods to remove those classes. Fix more logic to make tests pass. Disable enforcer plugin for good? Revive EmbeddedParameterConverter but don't completely phase out other code. Spotless. --- .../annotation/EmbeddedOperationParam.java | 117 ---- .../OperationParameterRangeType.java | 2 +- .../rest/server/method/BaseMethodBinding.java | 4 +- ...seMethodBindingMethodParameterBuilder.java | 21 +- .../method/EmbeddedOperationParameter.java | 623 ------------------ .../server/method/EmbeddedOperationUtils.java | 42 +- .../method/EmbeddedParameterConverter.java | 256 ++++--- .../fhir/rest/server/method/MethodUtil.java | 52 +- .../server/method/OperationMethodBinding.java | 6 +- hapi-fhir-storage-cr/pom.xml | 3 + .../fhir/cr/r4/measure/CareGapsParams.java | 1 - .../measure/EvaluateMeasureSingleParams.java | 1 - .../method/EmbeddedOperationUtilsTest.java | 20 +- .../rest/server/method/MethodUtilTest.java | 23 - 14 files changed, 220 insertions(+), 951 deletions(-) delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java delete mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java deleted file mode 100644 index 457f9e59cfdc..000000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParam.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * #%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 ca.uhn.fhir.model.primitive.StringDt; -import ca.uhn.fhir.rest.param.StringParam; -import org.hl7.fhir.instance.model.api.IBase; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Used by any method parameter within an class passed to an {@link Operation} method to be converted into an - * OperationEmbeddedParameter. - *

- * The class itself doesn't have an annotation, only its fields. - *

- * The method parameter in the operation also is explicitly not annotated - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) -public @interface EmbeddedOperationParam { - // LUKETODO: after writing the conformance code get rid of a bunch of cruft like MAX_UNLIMITED - - /** - * Value for {@link EmbeddedOperationParam#max()} indicating no maximum - */ - int MAX_UNLIMITED = -1; - - /** - * Value for {@link EmbeddedOperationParam#max()} indicating that the maximum will be inferred - * from the type. If the type is a single parameter type (e.g. StringDt, - * TokenParam, IBaseResource) the maximum will be - * 1. - *

- * If the type is a collection, e.g. - * List<StringDt> or List<TokenOrListParam> - * the maximum will be set to *. If the param is a search parameter - * "and" type, such as TokenAndListParam the maximum will also be - * set to * - *

- * - * @since 1.5 - */ - int MAX_DEFAULT = -2; - - /** - * The name of the parameter - */ - String name(); - - /** - * The type of the parameter. This will only have effect on @OperationParam - * annotations specified as values for {@link Operation#returnParameters()}, otherwise the - * value will be ignored. Value should be one of: - *
    - *
  • A resource type, e.g. Patient.class
  • - *
  • A datatype, e.g. {@link StringDt}.class or CodeableConceptDt.class - *
  • A RESTful search parameter type, e.g. {@link StringParam}.class - *
- */ - Class 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 Coding or - * base64Binary. This can be useful if you want to use a generic interface type - * on the actual method,such as {@link org.hl7.fhir.instance.model.api.IPrimitiveType} or - * {@link @org.hl7.fhir.instance.model.api.ICompositeType}. - */ - String typeName() default ""; - - /** - * The minimum number of repetitions allowed for this child (default is 0) - */ - int min() default 0; - - /** - * The maximum number of repetitions allowed for this child. Should be - * set to {@link #MAX_UNLIMITED} if there is no limit to the number of - * repetitions. See {@link #MAX_DEFAULT} for a description of the default - * behaviour. - */ - int max() default MAX_DEFAULT; -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java index 9e38850cbba4..e1bde701b000 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParameterRangeType.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.annotation; /** - * Used to indicate whether an {@link EmbeddedOperationParam} should be considered as part of a range of values, and if + * 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 { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 0de1575cdbaf..b73bf8981cae 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -259,7 +259,9 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th // LUKETODO: cleanup later ourLog.info( "1234: \nmethod: {}, \ninputParams: {}, \noutputParams: {}", - myMethod.getName(), theMethodParams, outputParams); + myMethod.getName(), + theMethodParams, + outputParams); return method.invoke(getProvider(), outputParams); } catch (InvocationTargetException e) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 694f3c0ecef2..77b493b70fb5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -21,7 +21,6 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -50,7 +49,7 @@ // LUKETODO: redo javadoc /** * Responsible for either passing to objects params straight through to the method call or converting them to - * fit within a class that has fields annotated with {@link EmbeddedOperationParam} and to also handle placement + * fit within a class that has cosntructor parameters annotated with {@link OperationParam} and to also handle placement * of {@link RequestDetails} in those params */ class BaseMethodBindingMethodParameterBuilder { @@ -209,7 +208,11 @@ private Object[] convertParamsIfNeeded( Parameter[] theConstructorParameters, Annotation[] theAnnotations) { - final Annotation[] annotations = Arrays.stream(theConstructorParameters).map(Parameter::getAnnotations).filter(array -> array.length == 1).map(array -> array[0]).toArray(Annotation[]::new); + final Annotation[] annotations = Arrays.stream(theConstructorParameters) + .map(Parameter::getAnnotations) + .filter(array -> array.length == 1) + .map(array -> array[0]) + .toArray(Annotation[]::new); if (!EmbeddedOperationUtils.hasAnyValidSourceTypeConversions( theMethodParamsWithoutRequestDetails, theConstructorParameters, annotations)) { @@ -231,7 +234,7 @@ private Object convertParamIfNeeded( final Object paramAtIndex = theMethodParamsWithoutRequestDetails[theIndex]; final Annotation annotation = theConstructorParameters[theIndex].getAnnotations()[0]; -// final Annotation annotation = theAnnotations[theIndex]; + // final Annotation annotation = theAnnotations[theIndex]; if (paramAtIndex == null) { return paramAtIndex; @@ -312,8 +315,7 @@ private Object[] buildMethodParamsInCorrectPositions(Object operationEmbeddedTyp } private void validMethodParamTypes( - Object[] theMethodParamsWithoutRequestDetails, - Parameter[] theConstructorParameters) { + Object[] theMethodParamsWithoutRequestDetails, Parameter[] theConstructorParameters) { if (theMethodParamsWithoutRequestDetails.length != theConstructorParameters.length) { final String error = String.format( @@ -343,10 +345,9 @@ private void validateMethodParamType(Object theMethodParam, Class theParamete final Class methodParamClass = theMethodParam.getClass(); - final Optional optOperationEmbeddedParam = - theAnnotation instanceof OperationParam - ? Optional.of((OperationParam) theAnnotation) - : Optional.empty(); + final Optional optOperationEmbeddedParam = theAnnotation instanceof OperationParam + ? Optional.of((OperationParam) theAnnotation) + : Optional.empty(); optOperationEmbeddedParam.ifPresent(embeddedParam -> { // LUKETODO: is this wise? diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java deleted file mode 100644 index 4dad5d418e38..000000000000 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationParameter.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * #%L - * HAPI FHIR - Server Framework - * %% - * 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.server.method; - -import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor; -import ca.uhn.fhir.context.*; -import ca.uhn.fhir.i18n.HapiLocalizer; -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.model.api.IQueryParameterAnd; -import ca.uhn.fhir.model.api.IQueryParameterOr; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; -import ca.uhn.fhir.rest.api.QualifiedParamList; -import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.api.ValidationModeEnum; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.BaseAndListParam; -import ca.uhn.fhir.rest.param.DateRangeParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.param.binder.CollectionBinder; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.ReflectionUtil; -import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.function.Consumer; -import java.util.*; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -// LUKETODO: consider deleting whatever code may be unused -/** - * Associated with a field annotated with {@link EmbeddedOperationParam} within a class passed to a method annotated with - * {@link Operation}. - */ -public class EmbeddedOperationParameter implements IParameter { - private static final Logger ourLog = LoggerFactory.getLogger(EmbeddedOperationParameter.class); - - // LUKETODO: do we need this to be separate or just reuse the one from OperationParameter? - // LUKETODO: if so, add conditional logic everywhere to use it - static final String REQUEST_CONTENTS_USERDATA_KEY = EmbeddedOperationParameter.class.getName() + "_PARSED_RESOURCE"; - - @SuppressWarnings("unchecked") - private static final Class[] COMPOSITE_TYPES = new Class[0]; - - private final FhirContext myContext; - private final String myName; - private final String myOperationName; - private boolean myAllowGet; - private IOperationParamConverter myConverter; - - @SuppressWarnings("rawtypes") - private Class myInnerCollectionType; - - private int myMax; - private final int myMin; - private Class myParameterType; - private String myParamType; - private SearchParameter mySearchParameterBinding; - private final String myDescription; - private final List myExampleValues; - private final Class mySourceType; - // LUKETODO: just pass the whole thing? - private final OperationParameterRangeType myRengeType; - - EmbeddedOperationParameter( - FhirContext theCtx, - String theOperationName, - String theParameterName, - int theMin, - int theMax, - String theDescription, - List theExampleValues, - Class theSourceType, - OperationParameterRangeType theRangeType) { - myOperationName = theOperationName; - myName = theParameterName; - myMin = theMin; - myMax = theMax; - myContext = theCtx; - myDescription = theDescription; - // LUKETODO: is this wise? - if (theSourceType == Void.class && myParameterType != null) { - mySourceType = myParameterType; - } else { - mySourceType = theSourceType; - } - myRengeType = theRangeType; - - List exampleValues = new ArrayList<>(); - if (theExampleValues != null) { - exampleValues.addAll(theExampleValues); - } - myExampleValues = Collections.unmodifiableList(exampleValues); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private void addValueToList(List matchingParamValues, Object values) { - if (values != null) { - if (BaseAndListParam.class.isAssignableFrom(myParameterType) && !matchingParamValues.isEmpty()) { - BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); - BaseAndListParam newAndList = (BaseAndListParam) values; - for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { - existing.addAnd(nextAnd); - } - } else { - matchingParamValues.add(values); - } - } - } - - protected FhirContext getContext() { - return myContext; - } - - public int getMax() { - return myMax; - } - - public int getMin() { - return myMin; - } - - public String getName() { - return myName; - } - - public String getParamType() { - return myParamType; - } - - public String getSearchParamType() { - if (mySearchParameterBinding != null) { - return mySearchParameterBinding.getParamType().getCode(); - } - return null; - } - - @VisibleForTesting - public Class getInnerCollectionType() { - return myInnerCollectionType; - } - - @VisibleForTesting - public String getOperationName() { - return myOperationName; - } - - @VisibleForTesting - public Class getSourceType() { - return mySourceType; - } - - @VisibleForTesting - public OperationParameterRangeType getRangeType() { - return myRengeType; - } - - @SuppressWarnings("unchecked") - @Override - public void initializeTypes( - Method theMethod, - Class> theOuterCollectionType, - Class> theInnerCollectionType, - Class theParameterType) { - FhirContext context = getContext(); - validateTypeIsAppropriateVersionForContext(theMethod, theParameterType, context, "parameter"); - - myParameterType = theParameterType; - if (theInnerCollectionType != null) { - myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName); - if (myMax == OperationParam.MAX_DEFAULT) { - myMax = OperationParam.MAX_UNLIMITED; - } - } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) { - if (myMax == OperationParam.MAX_DEFAULT) { - myMax = OperationParam.MAX_UNLIMITED; - } - } else { - if (myMax == OperationParam.MAX_DEFAULT) { - myMax = 1; - } - } - - boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers()); - - boolean isSearchParam = IQueryParameterType.class.isAssignableFrom(myParameterType) - || IQueryParameterOr.class.isAssignableFrom(myParameterType) - || IQueryParameterAnd.class.isAssignableFrom(myParameterType); - - /* - * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also - * extend this interface. I'm not sure if they should in the end.. but they do, so we - * exclude them. - */ - isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); - - myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) - || String.class.equals(myParameterType) - || String.class.equals(mySourceType) - || isSearchParam - || ValidationModeEnum.class.equals(myParameterType); - - /* - * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We - * should probably clean this up.. - */ - if (!myParameterType.equals(IBase.class) - && !myParameterType.equals(String.class) - && !EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRengeType)) { - if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { - myParamType = "Resource"; - } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { - myParamType = "Reference"; - myAllowGet = true; - } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { - myParamType = "Coding"; - myAllowGet = true; - } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) { - myParamType = "date"; - myMax = 2; - myAllowGet = true; - } else if (myParameterType.equals(ValidationModeEnum.class)) { - myParamType = "code"; - } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) { - myParamType = myContext - .getElementDefinition((Class) myParameterType) - .getName(); - } else if (isSearchParam) { - myParamType = "string"; - mySearchParameterBinding = new SearchParameter(myName, myMin > 0); - mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES); - mySearchParameterBinding.setType( - myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); - myConverter = new OperationParamConverter(); - } else { - // LUKETODO: claim new code - // LUKETODO: test the rangeType NOT_APPLICABLE scenario - final String error = String.format( - "%sInvalid type for @OperationEmbeddedParam on method: %s with sourceType: %s, parameterType: %s, and rangeType: %s", - Msg.code(999991), theMethod.getName(), mySourceType, myParameterType.getName(), myRengeType); - throw new ConfigurationException(error); - } - } - } - - public static void validateTypeIsAppropriateVersionForContext( - Method theMethod, Class theParameterType, FhirContext theContext, String theUseDescription) { - if (theParameterType != null) { - if (theParameterType.isInterface()) { - // TODO: we could probably be a bit more nuanced here but things like - // IBaseResource are often used and they aren't version specific - return; - } - - FhirVersionEnum elementVersion = FhirVersionEnum.determineVersionForType(theParameterType); - if (elementVersion != null) { - if (elementVersion != theContext.getVersion().getVersion()) { - // LUKETODO: claim new code - throw new ConfigurationException(Msg.code(9999992) + "Incorrect use of type " - + theParameterType.getSimpleName() + " as " + theUseDescription - + " type for method when theContext is for version " - + theContext.getVersion().getVersion().name() + " in method: " + theMethod.toString()); - } - } - } - } - - EmbeddedOperationParameter setConverter(IOperationParamConverter theConverter) { - myConverter = theConverter; - return this; - } - - private void throwWrongParamType(Object nextValue) { - // LUKETODO: claim new code - throw new InvalidRequestException(Msg.code(9999993) + "Request has parameter " + myName + " of type " - + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); - } - - @SuppressWarnings("unchecked") - @Override - public Object translateQueryParametersIntoServerArgument( - RequestDetails theRequest, BaseMethodBinding theMethodBinding) - throws InternalErrorException, InvalidRequestException { - List matchingParamValues = new ArrayList<>(); - - OperationMethodBinding method = (OperationMethodBinding) theMethodBinding; - - if (theRequest.getRequestType() == RequestTypeEnum.GET - || method.isManualRequestMode() - || method.isDeleteEnabled()) { - translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues); - } else { - translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues); - } - - if (matchingParamValues.isEmpty()) { - return null; - } - - if (myInnerCollectionType == null) { - return matchingParamValues.get(0); - } - - Collection retVal = ReflectionUtil.newInstance(myInnerCollectionType); - retVal.addAll(matchingParamValues); - return retVal; - } - - private void translateQueryParametersIntoServerArgumentForGet( - RequestDetails theRequest, List matchingParamValues) { - if (mySearchParameterBinding != null) { - - List params = new ArrayList<>(); - String nameWithQualifierColon = myName + ":"; - - for (String nextParamName : theRequest.getParameters().keySet()) { - String qualifier; - if (nextParamName.equals(myName)) { - qualifier = null; - } else if (nextParamName.startsWith(nameWithQualifierColon)) { - qualifier = nextParamName.substring(nextParamName.indexOf(':')); - } else { - // This is some other parameter, not the one bound by this instance - continue; - } - String[] values = theRequest.getParameters().get(nextParamName); - if (values != null) { - for (String nextValue : values) { - params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); - } - } - } - if (!params.isEmpty()) { - for (QualifiedParamList next : params) { - Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); - addValueToList(matchingParamValues, values); - } - } - - } else { - String[] paramValues = theRequest.getParameters().get(myName); - ourLog.info( - "1234: operation: {} paramName: {}, paramValues: {}", - theRequest.getOperation(), - myName, - Arrays.toString(paramValues)); - - if (paramValues != null && paramValues.length > 0) { - if (myAllowGet) { - - if (DateRangeParam.class.isAssignableFrom(myParameterType)) { - List parameters = new ArrayList<>(); - parameters.add(QualifiedParamList.singleton(paramValues[0])); - if (paramValues.length > 1) { - parameters.add(QualifiedParamList.singleton(paramValues[1])); - } - DateRangeParam dateRangeParam = new DateRangeParam(); - FhirContext ctx = theRequest.getServer().getFhirContext(); - dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters); - matchingParamValues.add(dateRangeParam); - - } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { - - processAllCommaSeparatedValues(paramValues, t -> { - IBaseReference param = (IBaseReference) ReflectionUtil.newInstance(myParameterType); - param.setReference(t); - matchingParamValues.add(param); - }); - - } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { - - processAllCommaSeparatedValues(paramValues, t -> { - TokenParam tokenParam = new TokenParam(); - tokenParam.setValueAsQueryToken(myContext, myName, null, t); - - IBaseCoding param = (IBaseCoding) ReflectionUtil.newInstance(myParameterType); - param.setSystem(tokenParam.getSystem()); - param.setCode(tokenParam.getValue()); - matchingParamValues.add(param); - }); - - // LUKETODO: comment to explain - // LUKETODO: call EmbeddedOperationUtils.isValidSourceTypeConversion ???? - } else if (String.class.isAssignableFrom(myParameterType) || String.class.equals(mySourceType)) { - - matchingParamValues.addAll(Arrays.asList(paramValues)); - - } else if (ValidationModeEnum.class.equals(myParameterType)) { - - if (isNotBlank(paramValues[0])) { - ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); - if (validationMode != null) { - matchingParamValues.add(validationMode); - } else { - throwInvalidMode(paramValues[0]); - } - } - - } else { - for (String nextValue : paramValues) { - FhirContext ctx = theRequest.getServer().getFhirContext(); - RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) - ctx.getElementDefinition(myParameterType.asSubclass(IBase.class)); - IPrimitiveType instance = def.newInstance(); - instance.setValueAsString(nextValue); - matchingParamValues.add(instance); - } - } - } else { - HapiLocalizer localizer = - theRequest.getServer().getFhirContext().getLocalizer(); - String msg = localizer.getMessage( - EmbeddedOperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); - // LUKETODO: claim new code - throw new MethodNotAllowedException(Msg.code(99999993) + msg, RequestTypeEnum.POST); - } - } - } - } - - /** - * This method is here to mediate between the POST form of operation parameters (i.e. elements within a Parameters - * resource) and the GET form (i.e. URL parameters). - *

- * Essentially we want to allow comma-separated values as is done with searches on URLs. - *

- */ - private void processAllCommaSeparatedValues(String[] theParamValues, Consumer theHandler) { - for (String nextValue : theParamValues) { - QualifiedParamList qualifiedParamList = - QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextValue); - for (String nextSplitValue : qualifiedParamList) { - theHandler.accept(nextSplitValue); - } - } - } - - private void translateQueryParametersIntoServerArgumentForPost( - RequestDetails theRequest, List matchingParamValues) { - // ourLog.info("1234: translateQueryParametersIntoServerArgumentForPost: operation: {}, matchingParamValues: - // {}", theRequest.getOperation(), matchingParamValues); - IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); - if (requestContents != null) { - RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); - if (def.getName().equals("Parameters")) { - - BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); - BaseRuntimeElementCompositeDefinition paramChildElem = - (BaseRuntimeElementCompositeDefinition) paramChild.getChildByName("parameter"); - - RuntimeChildPrimitiveDatatypeDefinition nameChild = - (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name"); - BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]"); - BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource"); - - IAccessor paramChildAccessor = paramChild.getAccessor(); - List values = paramChildAccessor.getValues(requestContents); - for (IBase nextParameter : values) { - List nextNames = nameChild.getAccessor().getValues(nextParameter); - if (nextNames != null && !nextNames.isEmpty()) { - IPrimitiveType nextName = (IPrimitiveType) nextNames.get(0); - if (myName.equals(nextName.getValueAsString())) { - - if (myParameterType.isAssignableFrom(nextParameter.getClass())) { - matchingParamValues.add(nextParameter); - } else { - List paramValues = - valueChild.getAccessor().getValues(nextParameter); - List paramResources = - resourceChild.getAccessor().getValues(nextParameter); - ourLog.info( - "1234: try to add values: operation: {}, myName: {}, nextParameter: {}, paramValues: {} matchingParamValues: {}", - theRequest.getOperation(), - myName, - nextParameter, - paramResources, - matchingParamValues); - // try to add values: operation: $evaluate-measures, myName: periodStart, - // nextParameter: - // org.hl7.fhir.r4.model.Parameters$ParametersParameterComponent@7682050a, paramValues: - // [] matchingParamValues: [] - // LUKETODO: some part of this code reacts badly to ZonedDateTime - // HAPI-1716: Resource class[java.time.ZonedDateTime] does not contain any valid - // HAPI-FHIR annotations - if (paramValues != null && !paramValues.isEmpty()) { - // paramValues non-empty: adding paramvalues: [DateType[2023-01-01]] - ourLog.info("1234: paramValues non-empty: adding paramvalues: {}", paramValues); - tryToAddValues(paramValues, matchingParamValues); - } else if (paramResources != null && !paramResources.isEmpty()) { - ourLog.info( - "1234: peramResources non-empty: adding peramResources: {}", - paramResources); - tryToAddValues(paramResources, matchingParamValues); - } - } - } - } - } - - } else { - - if (myParameterType.isAssignableFrom(requestContents.getClass())) { - tryToAddValues(Arrays.asList(requestContents), matchingParamValues); - } - } - } - } - - @SuppressWarnings("unchecked") - private void tryToAddValues(List theParamValues, List theMatchingParamValues) { - ourLog.info("1234:tryToAddValues: {}, {}", theParamValues, theMatchingParamValues); - for (Object nextValue : theParamValues) { - if (nextValue == null) { - continue; - } - if (myConverter != null) { - nextValue = myConverter.incomingServer(nextValue); - } - // LUKETODO: test this - if (myParameterType.equals(String.class) - || EmbeddedOperationUtils.isValidSourceTypeConversion(mySourceType, myParameterType, myRengeType)) { - if (nextValue instanceof IPrimitiveType) { - IPrimitiveType source = (IPrimitiveType) nextValue; - theMatchingParamValues.add(source.getValueAsString()); - continue; - } - } - if (!myParameterType.isAssignableFrom(nextValue.getClass())) { - Class sourceType = (Class) nextValue.getClass(); - Class targetType = (Class) myParameterType; - BaseRuntimeElementDefinition sourceTypeDef = myContext.getElementDefinition(sourceType); - BaseRuntimeElementDefinition targetTypeDef = myContext.getElementDefinition(targetType); - if (targetTypeDef instanceof IRuntimeDatatypeDefinition - && sourceTypeDef instanceof IRuntimeDatatypeDefinition) { - IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef; - if (targetTypeDtDef.isProfileOf(sourceType)) { - FhirTerser terser = myContext.newTerser(); - IBase newTarget = targetTypeDef.newInstance(); - terser.cloneInto((IBase) nextValue, newTarget, true); - theMatchingParamValues.add(newTarget); - continue; - } - } - throwWrongParamType(nextValue); - } - - addValueToList(theMatchingParamValues, nextValue); - } - } - - public String getDescription() { - return myDescription; - } - - public List getExampleValues() { - return myExampleValues; - } - - interface IOperationParamConverter { - - Object incomingServer(Object theObject); - - Object outgoingClient(Object theObject); - } - - class OperationParamConverter implements IOperationParamConverter { - - public OperationParamConverter() { - Validate.isTrue(mySearchParameterBinding != null); - } - - @Override - public Object incomingServer(Object theObject) { - IPrimitiveType obj = (IPrimitiveType) theObject; - List paramList = Collections.singletonList( - QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); - return mySearchParameterBinding.parse(myContext, paramList); - } - - @Override - public Object outgoingClient(Object theObject) { - IQueryParameterType obj = (IQueryParameterType) theObject; - IPrimitiveType retVal = - (IPrimitiveType) myContext.getElementDefinition("string").newInstance(); - retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); - return retVal; - } - } - - public static void throwInvalidMode(String paramValues) { - // LUKETODO: claim new code - throw new InvalidRequestException(Msg.code(99999994) + "Invalid mode value: \"" + paramValues + "\""); - } -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 089b8023d9dd..fbec50948993 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -22,10 +22,9 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; +import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; -import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; import java.lang.annotation.Annotation; @@ -44,21 +43,20 @@ import java.util.stream.IntStream; /** - * Common operations for any functionality that work with {@link EmbeddedOperationParam} + * Common operations for any functionality that work with {@link EmbeddedOperationParams} */ public class EmbeddedOperationUtils { private EmbeddedOperationUtils() {} - // LUKETODO: redo for OperationParam /** - * Validate that a constructor for a class with fields that are {@link EmbeddedOperationParam} declares its - * parameters in the same order as the fields are declared in the class. It also validates that the fields are + * Validate that for a class that has a {@link EmbeddableOperationParams} its constructor hass parameters that are + * {@link OperationParam}. It also validates that the fields are * final. It also takes into account Collections and generic types, as well as whether there is a source to * target type conversion, such as String to ZonedDateTime. * - * @param theParameterTypeWithOperationEmbeddedParam the class that has fields that are - * annotated with {@link EmbeddedOperationParam} + * @param theParameterTypeWithOperationEmbeddedParam the class that constructor params that are + * annotated with {@link OperationParam} * @return the constructor for the class */ static Constructor validateAndGetConstructor(Class theParameterTypeWithOperationEmbeddedParam) { @@ -84,11 +82,6 @@ static Constructor validateAndGetConstructor(Class theParameterTypeWithOpe return soleConstructor; } - static boolean hasAnyMethodParamsWithClassesWithFieldsWithEmbeddedOperationParams(Method theMethod) { - return ReflectionUtil.hasAnyMethodParamsWithClassesWithFieldsWithAnnotation( - theMethod, EmbeddedOperationParam.class); - } - // LUKETODO: javadoc static boolean hasAnyValidSourceTypeConversions( Object[] theMethodParamsWithoutRequestDetails, @@ -123,8 +116,8 @@ private static Boolean isValidSourceTypeConversion( final Annotation annotation = theAnnotations[theIndex]; if (annotation instanceof OperationParam) { - final OperationParam embeddedOperationParam = (OperationParam) annotation; - final OperationParameterRangeType operationParameterRangeType = embeddedOperationParam.rangeType(); + final OperationParam operationParam = (OperationParam) annotation; + final OperationParameterRangeType operationParameterRangeType = operationParam.rangeType(); if (isValidSourceTypeConversion(methodParamClass, constructorParamType, operationParameterRangeType)) { return true; @@ -175,6 +168,7 @@ private static boolean hasEmbeddableOperationParamsAnnotation(Class theMethod } private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { + final Parameter[] constructorParameters = theConstructor.getParameters(); final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); if (constructorParameterTypes.length != theDeclaredFields.length) { @@ -186,8 +180,24 @@ private static void validateConstructorArgs(Constructor theConstructor, Field final Type[] constructorGenericParameterTypes = theConstructor.getGenericParameterTypes(); - for (int index = 0; index < constructorParameterTypes.length; index++) { + for (int index = 0; index < constructorParameters.length; index++) { + final Parameter constructorParameterAtIndex = constructorParameters[index]; final Class constructorParameterTypeAtIndex = constructorParameterTypes[index]; + + final Annotation[] constructorParamAnnotations = constructorParameterAtIndex.getAnnotations(); + + if (constructorParamAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for constructor class: %s param: %s", + Msg.code(9999926), theConstructor.getDeclaringClass(), constructorParameterAtIndex)); + } + + if (constructorParamAnnotations.length > 1) { + throw new ConfigurationException(String.format( + "%sMore than one annotation for constructor: %s param: %s ", + Msg.code(999998), theConstructor, constructorParameterTypeAtIndex)); + } + final Field declaredFieldAtIndex = theDeclaredFields[index]; final Class fieldTypeAtIndex = declaredFieldAtIndex.getType(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index a54d4c129c4e..0c4d53484637 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -22,155 +22,149 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; import jakarta.annotation.Nonnull; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import static org.slf4j.LoggerFactory.*; +import static org.slf4j.LoggerFactory.getLogger; -// LUKETODO: redo javadoc -// LUKETODO: convert to new design /** - * Leveraged by {@link MethodUtil} exclusively to convert {@link EmbeddedOperationParam} parameters for a method to - * either a {@link NullParameter} or an {@link EmbeddedOperationParam}. + * Leveraged by {@link MethodUtil} exclusively to convert {@link OperationParam} parameters for a method to + * either a {@link NullParameter} or an {@link OperationParam}. */ public class EmbeddedParameterConverter { private static final org.slf4j.Logger ourLog = getLogger(EmbeddedParameterConverter.class); -// -// private final FhirContext myContext; -// private final Method myMethod; -// private final Operation myOperation; -// private final Class myOperationEmbeddedType; -// -// public EmbeddedParameterConverter( -// FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { -// myContext = theContext; -// myMethod = theMethod; -// myOperation = theOperation; -// myOperationEmbeddedType = theOperationEmbeddedType; -// } -// -// List convert() { -// return Arrays.stream(validateConstructorArgsAndReturnFields()) -// .map(this::convertField) -// .collect(Collectors.toUnmodifiableList()); -// } -// -// private Field[] validateConstructorArgsAndReturnFields() { -// EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); -// -// return myOperationEmbeddedType.getDeclaredFields(); -// } -// -// private EmbeddedParameterConverterContext convertField(Field theField) { -// final String fieldName = theField.getName(); -// final Class fieldType = theField.getType(); -// final Annotation[] fieldAnnotations = theField.getAnnotations(); -// -// if (fieldAnnotations.length < 1) { -// throw new ConfigurationException(String.format( -// "%sNo annotations for field: %s for method: %s", Msg.code(9999926), fieldName, myMethod.getName())); -// } -// -// if (fieldAnnotations.length > 1) { -// throw new ConfigurationException(String.format( -// "%sMore than one annotation for field: %s for method: %s", -// Msg.code(999998), fieldName, myMethod.getName())); -// } -// -// final Annotation fieldAnnotation = fieldAnnotations[0]; -// -// if (fieldAnnotation instanceof IdParam) { -// return EmbeddedParameterConverterContext.forParameter(new NullParameter()); -// } else if (fieldAnnotation instanceof EmbeddedOperationParam) { -// final ParamInitializationContext paramContext = -// buildParamContext(fieldType, theField, (EmbeddedOperationParam) fieldAnnotation); -// -// return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); -// } else { -// final String error = String.format( -// "%sUnsupported annotation type: %s for a class: %s with OperationEmbeddedParams which is part of method: %s: ", -// Msg.code(912732197), myOperationEmbeddedType, fieldAnnotation.annotationType(), myMethod.getName()); -// -// throw new ConfigurationException(error); -// } -// } -// -// private ParamInitializationContext buildParamContext( -// Class theFieldType, Field theField, EmbeddedOperationParam theEmbeddedOperationParam) { -// -// final EmbeddedOperationParameter embeddedOperationParameter = -// getOperationEmbeddedParameter(theEmbeddedOperationParam); -// -// Class parameterType = theFieldType; -// Class> outerCollectionType = null; -// Class> innerCollectionType = null; -// -// // Flat collection -// if (Collection.class.isAssignableFrom(parameterType)) { -// innerCollectionType = unsafeCast(parameterType); -// parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); -// if (parameterType == null) { -// final String error = String.format( -// "%s Cannot find generic type for field: %s in class: %s for method: %s", -// Msg.code(724612469), -// theField.getName(), -// theField.getDeclaringClass().getCanonicalName(), -// myMethod.getName()); -// throw new ConfigurationException(error); -// } -// -// // Collection of a Collection: Permitted -// if (Collection.class.isAssignableFrom(parameterType)) { -// outerCollectionType = innerCollectionType; -// innerCollectionType = unsafeCast(parameterType); -// parameterType = ReflectionUtil.getGenericCollectionTypeOfField(theField); -// } -// -// // Collection of a Collection of a Collection: Prohibited -// if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { -// final String error = String.format( -// "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for method: %s", -// Msg.code(724612469), -// theField.getName(), -// theField.getDeclaringClass().getCanonicalName(), -// myMethod.getName()); -// throw new ConfigurationException(error); -// } -// } -// -// // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later -// -// return new ParamInitializationContext( -// embeddedOperationParameter, parameterType, outerCollectionType, innerCollectionType); -// } -// -// @Nonnull -// private EmbeddedOperationParameter getOperationEmbeddedParameter(EmbeddedOperationParam operationParam) { -// final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; -// -// return new EmbeddedOperationParameter( -// myContext, -// myOperation.name(), -// operationParam.name(), -// operationParam.min(), -// operationParam.max(), -// ParametersUtil.extractDescription(fieldAnnotationArray), -// ParametersUtil.extractExamples(fieldAnnotationArray), -// operationParam.sourceType(), -// operationParam.rangeType()); -// } + + private final FhirContext myContext; + private final Method myMethod; + private final Operation myOperation; + private final Class myOperationEmbeddedType; + private final Constructor myConstructor; + + public EmbeddedParameterConverter( + FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { + myContext = theContext; + myMethod = theMethod; + myOperation = theOperation; + myOperationEmbeddedType = theOperationEmbeddedType; + myConstructor = EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); + } + + List convert() { + return Arrays.stream(myConstructor.getParameters()) + .map(this::convertConstructorParameter) + .collect(Collectors.toUnmodifiableList()); + } + + private EmbeddedParameterConverterContext convertConstructorParameter(Parameter theConstructorParameter) { + final Class constructorParamType = theConstructorParameter.getType(); + final Annotation[] constructorParamAnnotations = theConstructorParameter.getAnnotations(); + + if (constructorParamAnnotations.length < 1) { + throw new ConfigurationException(String.format( + "%sNo annotations for field: %s for method: %s", + Msg.code(9999926), constructorParamType, myMethod.getName())); + } + + final Annotation constructorParamAnnotation = constructorParamAnnotations[0]; + + if (constructorParamAnnotation instanceof IdParam) { + return EmbeddedParameterConverterContext.forParameter(new NullParameter()); + } else if (constructorParamAnnotation instanceof OperationParam) { + final OperationParameter operationParameter = + getOperationParameter((OperationParam) constructorParamAnnotation); + + final ParamInitializationContext paramContext = + buildParamContext(theConstructorParameter, operationParameter); + + return EmbeddedParameterConverterContext.forEmbeddedContext(paramContext); + } else { + final String error = String.format( + "%sUnsupported annotation type: %s for a class: %s with OperationEmbeddedParams which is part of method: %s: ", + Msg.code(912732197), + myOperationEmbeddedType, + constructorParamAnnotation.annotationType(), + myMethod.getName()); + + throw new ConfigurationException(error); + } + } + + private ParamInitializationContext buildParamContext( + Parameter theConstructorParameter, OperationParameter theOperationParameter) { + + final Class genericParameter = + ReflectionUtil.getGenericCollectionTypeOfConstructorParameter(myConstructor, theConstructorParameter); + + Class parameterType = theConstructorParameter.getType(); + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + // Flat collection + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = unsafeCast(parameterType); + parameterType = genericParameter; + if (parameterType == null) { + final String error = String.format( + "%s Cannot find generic type for field: %s in class: %s for constructor: %s", + Msg.code(724612469), + theConstructorParameter.getName(), + theConstructorParameter.getClass().getCanonicalName(), + myConstructor.getName()); + throw new ConfigurationException(error); + } + + // Collection of a Collection: Permitted + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = unsafeCast(parameterType); + } + + // Collection of a Collection of a Collection: Prohibited + if (Collection.class.isAssignableFrom(parameterType)) { + final String error = String.format( + "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for constructor: %s", + Msg.code(724612469), + theConstructorParameter.getName(), + theConstructorParameter.getClass().getCanonicalName(), + myConstructor.getName()); + throw new ConfigurationException(error); + } + } + + // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later + + return new ParamInitializationContext( + theOperationParameter, parameterType, outerCollectionType, innerCollectionType); + } + + @Nonnull + private OperationParameter getOperationParameter(OperationParam operationParam) { + final Annotation[] fieldAnnotationArray = new Annotation[] {operationParam}; + + return new OperationParameter( + myContext, + myOperation.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + ParametersUtil.extractDescription(fieldAnnotationArray), + ParametersUtil.extractExamples(fieldAnnotationArray), + operationParam.sourceType(), + operationParam.rangeType()); + } @SuppressWarnings("unchecked") private static T unsafeCast(Object theObject) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 3b8cb389c313..fc5469a16cf1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -380,6 +380,12 @@ public static List getResourceParameters( Msg.code(9999927), methodToUse.getName())); } + final EmbeddedParameterConverter embeddedParameterConverter = + new EmbeddedParameterConverter(theContext, theMethod, op, embeddedParamsClasses.get(0)); + + final List outerContexts = + embeddedParameterConverter.convert(); + final Class soleEmbeddedParamClass = embeddedParamsClasses.get(0); final Constructor[] constructorsForEmbeddableOperationParams = @@ -458,29 +464,29 @@ public static List getResourceParameters( } // LUKETODO: extract the pattent from the code below then delete: -// if (!operationEmbeddedTypes.isEmpty()) { -// final EmbeddedParameterConverter embeddedParameterConverter = -// new EmbeddedParameterConverter( -// theContext, theMethod, op, operationEmbeddedTypes.get(0)); -// -// final List outerContexts = -// embeddedParameterConverter.convert(); -// -// for (EmbeddedParameterConverterContext outerContext : outerContexts) { -// if (outerContext.getParameter() != null) { -// parameters.add(outerContext.getParameter()); -// } -// final ParamInitializationContext paramContext = outerContext.getParamContext(); -// -// if (paramContext != null) { -// paramContexts.add(paramContext); -// -// // N.B. This a hack used only to pass the null check below, which is crucial to the -// // non-embedded params logic -// param = paramContext.getParam(); -// } -// } -// } + // if (!operationEmbeddedTypes.isEmpty()) { + // final EmbeddedParameterConverter embeddedParameterConverter = + // new EmbeddedParameterConverter( + // theContext, theMethod, op, operationEmbeddedTypes.get(0)); + // + // final List outerContexts = + // embeddedParameterConverter.convert(); + // + // for (EmbeddedParameterConverterContext outerContext : outerContexts) { + // if (outerContext.getParameter() != null) { + // parameters.add(outerContext.getParameter()); + // } + // final ParamInitializationContext paramContext = outerContext.getParamContext(); + // + // if (paramContext != null) { + // paramContexts.add(paramContext); + // + // // N.B. This a hack used only to pass the null check below, which is crucial to the + // // non-embedded params logic + // param = paramContext.getParam(); + // } + // } + // } } else if (nextAnnotation instanceof Validate.Mode) { if (!parameterType.equals(ValidationModeEnum.class)) { throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index caa37af4843b..507210bedb75 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -451,7 +451,8 @@ public String getCanonicalUrl() { } private OperationIdParamDetails findIdParameterDetails(Method theMethod, FhirContext theContext) { - final List> operationEmbeddedTypes = EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(theMethod); + final List> operationEmbeddedTypes = + EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(theMethod); if (!operationEmbeddedTypes.isEmpty()) { return findIdParamIndexForTypeWithEmbeddedParams(theMethod, operationEmbeddedTypes, theContext); @@ -496,7 +497,8 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( // skip } else { // LUKETODO: fix - final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(typeWithEmbeddedParams); + final Constructor constructor = + EmbeddedOperationUtils.validateAndGetConstructor(typeWithEmbeddedParams); final Parameter[] constructorParams = constructor.getParameters(); diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index fb70f0f0f0ea..1e9955fe6b35 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -20,6 +20,9 @@ 4.10.1 5.7.8 + + true + diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 3c0ddcca24d4..befb34d71ddc 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.BooleanType; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index e51dfe0a9d87..61cc0a1eb763 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; -import ca.uhn.fhir.rest.annotation.EmbeddedOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java index d8df7087e291..3cca93ba5b2b 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; +import ca.uhn.fhir.rest.annotation.OperationParam; import org.junit.jupiter.api.Test; import java.lang.reflect.Constructor; @@ -12,11 +14,16 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class EmbeddedOperationUtilsTest { + @EmbeddableOperationParams private static class SimpleFieldsAndConstructorInOrder { private final String myParam1; private final int myParam2; - public SimpleFieldsAndConstructorInOrder(String theParam1, int theParam2) { + public SimpleFieldsAndConstructorInOrder( + @OperationParam(name = "param1") + String theParam1, + @OperationParam(name = "param2") + int theParam2) { myParam1 = theParam1; myParam2 = theParam2; } @@ -99,13 +106,22 @@ public int hashCode() { } } + @EmbeddableOperationParams private static class WithGenericFieldsAndConstructorInOrder { private final String myParam1; private final int myParam2; private final List myParam3; private final List myParam4; - public WithGenericFieldsAndConstructorInOrder(String theParam1, int theParam2, List theParam3, List theParam4) { + public WithGenericFieldsAndConstructorInOrder( + @OperationParam(name = "param1") + String theParam1, + @OperationParam(name = "param2") + int theParam2, + @OperationParam(name = "param3") + List theParam3, + @OperationParam(name = "param4") + List theParam4) { myParam1 = theParam1; myParam2 = theParam2; myParam3 = theParam3; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 66cc1216e843..6e93580537b7 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -408,17 +408,6 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I return true; } - if (theExpectedParameter instanceof EmbeddedOperationParameterToAssert expectedEmbeddedOperationParameter && theActualParameter instanceof EmbeddedOperationParameter actualEmbeddedOperationParameter) { - assertThat(actualEmbeddedOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedEmbeddedOperationParameter.myContext().getVersion().getVersion()); - assertThat(actualEmbeddedOperationParameter.getName()).isEqualTo(expectedEmbeddedOperationParameter.myName()); - assertThat(actualEmbeddedOperationParameter.getParamType()).isEqualTo(expectedEmbeddedOperationParameter.myParamType()); - assertThat(actualEmbeddedOperationParameter.getInnerCollectionType()).isEqualTo(expectedEmbeddedOperationParameter.myInnerCollectionType()); - assertThat(actualEmbeddedOperationParameter.getSourceType()).isEqualTo(expectedEmbeddedOperationParameter.myTypeToConvertFrom()); - assertThat(actualEmbeddedOperationParameter.getRangeType()).isEqualTo(expectedEmbeddedOperationParameter.myRangeType()); - - return true; - } - return false; } @@ -441,16 +430,4 @@ private record OperationParameterToAssert( Class myTypeToConvertFrom, OperationParameterRangeType myRangeType) implements IParameterToAssert { } - - private record EmbeddedOperationParameterToAssert( - FhirContext myContext, - String myName, - String myOperationName, - @SuppressWarnings("rawtypes") - Class myInnerCollectionType, - Class myParameterType, - String myParamType, - Class myTypeToConvertFrom, - OperationParameterRangeType myRangeType) implements IParameterToAssert { - } } From f299cfaf0cf8cf51707133f25de7f7e84c9336c4 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Jan 2025 14:48:44 -0500 Subject: [PATCH 59/75] Carve out and delete all extraneous code from MethodUtil to EmbeddedParameterConverter. Comment out enforcer plugin. --- .../method/EmbeddedParameterConverter.java | 30 ++- .../fhir/rest/server/method/MethodUtil.java | 188 +----------------- hapi-fhir-storage-cr/pom.xml | 58 +++--- 3 files changed, 67 insertions(+), 209 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 0c4d53484637..6bf6a9e62844 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -54,11 +55,36 @@ public class EmbeddedParameterConverter { private final Constructor myConstructor; public EmbeddedParameterConverter( - FhirContext theContext, Method theMethod, Operation theOperation, Class theOperationEmbeddedType) { + FhirContext theContext, Method theMethod, Operation theOperation) { + + if (theOperation == null) { + throw new ConfigurationException(Msg.code(846192641) + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + + theMethod.toGenericString()); + } + + final List> embeddedParamsClasses = Arrays.stream(theMethod.getParameterTypes()) + .filter(paramType -> paramType.isAnnotationPresent(EmbeddableOperationParams.class)) + .collect(Collectors.toUnmodifiableList()); + + // LUKETODO; better error? + if (embeddedParamsClasses.isEmpty()) { + throw new ConfigurationException(String.format( + "%sThere is no param with @EmbeddableOperationParams is supported for now for method: %s", + Msg.code(9999924), theMethod.getName())); + } + + // LUKETODO; better error? + if (embeddedParamsClasses.size() > 1) { + throw new ConfigurationException(String.format( + "%sMore than one param with with @EmbeddableOperationParams for method: %s", + Msg.code(9999927), theMethod.getName())); + } + myContext = theContext; myMethod = theMethod; myOperation = theOperation; - myOperationEmbeddedType = theOperationEmbeddedType; + myOperationEmbeddedType = embeddedParamsClasses.get(0); myConstructor = EmbeddedOperationUtils.validateAndGetConstructor(myOperationEmbeddedType); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index fc5469a16cf1..9caa55c01423 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -354,139 +354,24 @@ public static List getResourceParameters( parameterType = newParameterType; } } else if (nextAnnotation instanceof EmbeddedOperationParams) { - // LUKETODO: cleanup - // NEW - if (op == null) { - throw new ConfigurationException(Msg.code(846192641) - + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " - + methodToUse.toGenericString()); - } - - final List> embeddedParamsClasses = Arrays.stream(methodToUse.getParameterTypes()) - .filter(paramType -> paramType.isAnnotationPresent(EmbeddableOperationParams.class)) - .collect(Collectors.toUnmodifiableList()); - - // LUKETODO; better error? - if (embeddedParamsClasses.isEmpty()) { - throw new ConfigurationException(String.format( - "%sThere is no param with @EmbeddableOperationParams is supported for now for method: %s", - Msg.code(9999924), methodToUse.getName())); - } - - // LUKETODO; better error? - if (embeddedParamsClasses.size() > 1) { - throw new ConfigurationException(String.format( - "%sMore than one param with with @EmbeddableOperationParams for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - final EmbeddedParameterConverter embeddedParameterConverter = - new EmbeddedParameterConverter(theContext, theMethod, op, embeddedParamsClasses.get(0)); - - final List outerContexts = - embeddedParameterConverter.convert(); - - final Class soleEmbeddedParamClass = embeddedParamsClasses.get(0); + new EmbeddedParameterConverter(theContext, theMethod, op); - final Constructor[] constructorsForEmbeddableOperationParams = - soleEmbeddedParamClass.getConstructors(); - - // LUKETODO; better error? - if (constructorsForEmbeddableOperationParams.length == 0) { - throw new ConfigurationException(String.format( - "%sThere is no constructor with @EmbeddableOperationParams is supported for now for method: %s", - Msg.code(9999924), methodToUse.getName())); - } - - // LUKETODO; better error? - if (constructorsForEmbeddableOperationParams.length > 1) { - throw new ConfigurationException(String.format( - "%sOnly one constructor with @EmbeddableOperationParams is supported but there is mulitple for method: %s", - Msg.code(9999927), methodToUse.getName())); - } - - final Constructor constructor = constructorsForEmbeddableOperationParams[0]; - - final Parameter[] constructorParams = constructor.getParameters(); - - for (Parameter constructorParam : constructorParams) { - final Annotation[] annotations = constructorParam.getAnnotations(); - - // LUKETODO: test - if (annotations.length == 0) { - throw new ConfigurationException(String.format( - "%s Constructor params have no annotation for embedded params class: %s and method: %s", - Msg.code(9999937), - constructor.getDeclaringClass().getName(), - methodToUse.getName())); + for (EmbeddedParameterConverterContext outerContext : embeddedParameterConverter.convert()) { + if (outerContext.getParameter() != null) { + parameters.add(outerContext.getParameter()); } - // LUKETODO: test - if (annotations.length > 1) { - throw new ConfigurationException(String.format( - "%s Constructor params have more than one annotation for embedded params: %s and method: %s", - Msg.code(9999947), - constructor.getDeclaringClass().getName(), - methodToUse.getName())); - } + final ParamInitializationContext paramContext = outerContext.getParamContext(); - final Annotation soleAnnotation = annotations[0]; - - IParameter innerParam = null; - if (soleAnnotation instanceof IdParam) { - // LUKETODO: we're missing this parameter when we build the method binding - innerParam = new NullParameter(); - // Need to add this explicitly - parameters.add(innerParam); - } else if (soleAnnotation instanceof OperationParam) { - final OperationParam operationParam = (OperationParam) soleAnnotation; - final String description = ParametersUtil.extractDescription(annotations); - final List examples = ParametersUtil.extractExamples(annotations); - final OperationParameter operationParameter = new OperationParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples, - operationParam.sourceType(), - operationParam.rangeType()); - - final ParamInitializationContext paramContext = - buildParamContext(constructor, constructorParam, operationParameter); - - innerParam = operationParameter; + if (paramContext != null) { paramContexts.add(paramContext); - } - param = innerParam; + // N.B. This a hack used only to pass the null check below, which is crucial to the + // non-embedded params logic + param = paramContext.getParam(); + } } - - // LUKETODO: extract the pattent from the code below then delete: - // if (!operationEmbeddedTypes.isEmpty()) { - // final EmbeddedParameterConverter embeddedParameterConverter = - // new EmbeddedParameterConverter( - // theContext, theMethod, op, operationEmbeddedTypes.get(0)); - // - // final List outerContexts = - // embeddedParameterConverter.convert(); - // - // for (EmbeddedParameterConverterContext outerContext : outerContexts) { - // if (outerContext.getParameter() != null) { - // parameters.add(outerContext.getParameter()); - // } - // final ParamInitializationContext paramContext = outerContext.getParamContext(); - // - // if (paramContext != null) { - // paramContexts.add(paramContext); - // - // // N.B. This a hack used only to pass the null check below, which is crucial to the - // // non-embedded params logic - // param = paramContext.getParam(); - // } - // } - // } } else if (nextAnnotation instanceof Validate.Mode) { if (!parameterType.equals(ValidationModeEnum.class)) { throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" @@ -592,57 +477,4 @@ public Object outgoingClient(Object theObject) { } return parameters; } - - private static ParamInitializationContext buildParamContext( - Constructor theConstructor, Parameter theConstructorParameter, OperationParameter theOperationParam) { - - final Class genericParameter = - ReflectionUtil.getGenericCollectionTypeOfConstructorParameter(theConstructor, theConstructorParameter); - - Class parameterType = theConstructorParameter.getType(); - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - // Flat collection - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = unsafeCast(parameterType); - parameterType = genericParameter; - if (parameterType == null) { - final String error = String.format( - "%s Cannot find generic type for field: %s in class: %s for constructor: %s", - Msg.code(724612469), - theConstructorParameter.getName(), - theConstructorParameter.getClass().getCanonicalName(), - theConstructor.getName()); - throw new ConfigurationException(error); - } - - // Collection of a Collection: Permitted - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = unsafeCast(parameterType); - } - - // Collection of a Collection of a Collection: Prohibited - if (Collection.class.isAssignableFrom(parameterType)) { - final String error = String.format( - "%sInvalid generic type (a collection of a collection of a collection) for field: %s in class: %s for constructor: %s", - Msg.code(724612469), - theConstructorParameter.getName(), - theConstructorParameter.getClass().getCanonicalName(), - theConstructor.getName()); - throw new ConfigurationException(error); - } - } - - // TODO: LD: Don't worry about the OperationEmbeddedParam.type() for now until we chose to implement it later - - return new ParamInitializationContext( - theOperationParam, parameterType, outerCollectionType, innerCollectionType); - } - - @SuppressWarnings("unchecked") - private static T unsafeCast(Object theObject) { - return (T) theObject; - } } diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index 1e9955fe6b35..fcffd0323c37 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -209,35 +209,35 @@ - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-no-snapshot-cr-dependencies - - enforce - - verify - - - - No Clinical Reasoning Snapshots Allowed! - - *:* - - - org.opencds.cqf:* - org.opencds.cqf.cql:* - org.opencds.cqf.fhir:* - info.cqframework:* - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b6977df95a09a754bb112500f64109c4fbc8bead Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 11:15:31 -0500 Subject: [PATCH 60/75] Cleanup. javadoc. --- .../annotation/EmbeddableOperationParams.java | 6 +- .../annotation/EmbeddedOperationParams.java | 6 +- .../rest/server/method/BaseMethodBinding.java | 11 +- ...seMethodBindingMethodParameterBuilder.java | 46 ++--- .../server/method/EmbeddedOperationUtils.java | 78 ++++---- .../fhir/rest/server/method/MethodUtil.java | 18 +- .../server/method/OperationMethodBinding.java | 2 - .../measure/EvaluateMeasureSingleParams.java | 1 - ...thodBindingMethodParameterBuilderTest.java | 4 +- .../EmbeddedParamsInnerClassesAndMethods.java | 19 ++ .../rest/server/method/MethodUtilTest.java | 182 ++++-------------- 11 files changed, 133 insertions(+), 240 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java index f209297fd9e7..efcf5d27efb7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java @@ -24,7 +24,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -// LUKETODO: javadoc + +/** + * 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 {} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java index 09478ed8855c..f5f6708be24d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddedOperationParams.java @@ -24,7 +24,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -// LUKETODO: javadoc +/** + * 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 {} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index b73bf8981cae..a18e2d7dc6ad 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -254,16 +254,7 @@ protected final Object invokeServerMethod(RequestDetails theRequest, Object[] th final BaseMethodBindingMethodParameterBuilder baseMethodBindingMethodParameterBuilder = new BaseMethodBindingMethodParameterBuilder(method, theRequest, theMethodParams); - final Object[] outputParams = baseMethodBindingMethodParameterBuilder.build(); - - // LUKETODO: cleanup later - ourLog.info( - "1234: \nmethod: {}, \ninputParams: {}, \noutputParams: {}", - myMethod.getName(), - theMethodParams, - outputParams); - - return method.invoke(getProvider(), outputParams); + return method.invoke(getProvider(), baseMethodBindingMethodParameterBuilder.build()); } catch (InvocationTargetException e) { if (e.getCause() instanceof BaseServerResponseException) { throw (BaseServerResponseException) e.getCause(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 77b493b70fb5..e97d686fde22 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -46,17 +46,15 @@ import static java.util.function.Predicate.not; -// LUKETODO: redo javadoc /** * Responsible for either passing to objects params straight through to the method call or converting them to - * fit within a class that has cosntructor parameters annotated with {@link OperationParam} and to also handle placement + * fit within a class that has constructor parameters annotated with {@link OperationParam} and to also handle placement * of {@link RequestDetails} in those params */ class BaseMethodBindingMethodParameterBuilder { private static final Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); - // LUKETODO: constructor param or something? private final StringTimePeriodHandler myStringTimePeriodHandler = new StringTimePeriodHandler(ZoneOffset.UTC); private final Method myMethod; @@ -187,8 +185,6 @@ private Object buildOperationEmbeddedObject( final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(theParameterTypeWithOperationEmbeddedParam); - // LUKETODO: redo this method in the new constructor param world - // LUKETODO: off by one error final Object[] methodParamsWithoutRequestDetails = cloneWithRemovedRequestDetails(theMethodParams); final Annotation[] constructorAnnotations = constructor.getAnnotations(); @@ -221,7 +217,7 @@ private Object[] convertParamsIfNeeded( return IntStream.range(0, theMethodParamsWithoutRequestDetails.length) .mapToObj(index -> convertParamIfNeeded( - theMethodParamsWithoutRequestDetails, theConstructorParameters, theAnnotations, index)) + theMethodParamsWithoutRequestDetails, theConstructorParameters, index)) .toArray(Object[]::new); } @@ -229,17 +225,18 @@ private Object[] convertParamsIfNeeded( private Object convertParamIfNeeded( Object[] theMethodParamsWithoutRequestDetails, Parameter[] theConstructorParameters, - Annotation[] theAnnotations, int theIndex) { final Object paramAtIndex = theMethodParamsWithoutRequestDetails[theIndex]; - final Annotation annotation = theConstructorParameters[theIndex].getAnnotations()[0]; - // final Annotation annotation = theAnnotations[theIndex]; if (paramAtIndex == null) { return paramAtIndex; } + final Parameter constructorParameter = theConstructorParameters[theIndex]; + // Ne already validated that there is at least one annotation earlier + final Annotation annotation = constructorParameter.getAnnotations()[0]; + if (!(annotation instanceof OperationParam)) { return paramAtIndex; } @@ -247,7 +244,6 @@ private Object convertParamIfNeeded( final OperationParam operationParamAtIndex = (OperationParam) annotation; final Class paramClassAtIndex = paramAtIndex.getClass(); final OperationParameterRangeType rangeType = operationParamAtIndex.rangeType(); - final Parameter constructorParameter = theConstructorParameters[theIndex]; final Class constructorParameterType = constructorParameter.getType(); if (EmbeddedOperationUtils.isValidSourceTypeConversion( @@ -259,8 +255,8 @@ private Object convertParamIfNeeded( case END: return myStringTimePeriodHandler.getEndZonedDateTime(paramAtIndexAsString, myRequestDetails); default: - // LUKETODO: message, code, etc - throw new IllegalArgumentException(); + // This should never happen + throw new IllegalArgumentException(Msg.code(217312) + "Invalid range type: " + rangeType); } } else { return paramAtIndex; @@ -330,13 +326,11 @@ private void validMethodParamTypes( for (int index = 0; index < theMethodParamsWithoutRequestDetails.length; index++) { validateMethodParamType( theMethodParamsWithoutRequestDetails[index], - theConstructorParameters[index].getType(), - // LUKETODO: fix by not passing this directly - theConstructorParameters[index].getAnnotations()[0]); + theConstructorParameters[index]); } } - private void validateMethodParamType(Object theMethodParam, Class theParameterClass, Annotation theAnnotation) { + private void validateMethodParamType(Object theMethodParam, Parameter theConstructorParameter) { if (theMethodParam == null) { // argument is null, so we can't the type, so skip it: @@ -345,12 +339,10 @@ private void validateMethodParamType(Object theMethodParam, Class theParamete final Class methodParamClass = theMethodParam.getClass(); - final Optional optOperationEmbeddedParam = theAnnotation instanceof OperationParam - ? Optional.of((OperationParam) theAnnotation) - : Optional.empty(); + final Optional optOperationEmbeddedParam = + Optional.ofNullable(theConstructorParameter.getAnnotation(OperationParam.class)); optOperationEmbeddedParam.ifPresent(embeddedParam -> { - // LUKETODO: is this wise? if (embeddedParam.sourceType() != Void.class && methodParamClass != embeddedParam.sourceType()) { final String error = String.format( "%sMismatch between methodParamClass: %s and OperationEmbeddedParam source type: %s for method: %s", @@ -359,26 +351,28 @@ private void validateMethodParamType(Object theMethodParam, Class theParamete } }); + final Class parameterType = theConstructorParameter.getType(); + if (Collection.class.isAssignableFrom(methodParamClass) - || Collection.class.isAssignableFrom(theParameterClass)) { + || Collection.class.isAssignableFrom(parameterType)) { // ex: List and ArrayList - if (methodParamClass.isAssignableFrom(theParameterClass)) { + if (methodParamClass.isAssignableFrom(parameterType)) { final String error = String.format( "%sMismatch between methodParamClass: %s and parameterClassAtIndex: %s for method: %s", - Msg.code(236146124), methodParamClass, theParameterClass, myMethod.getName()); + Msg.code(236146124), methodParamClass, parameterType, myMethod.getName()); throw new InternalErrorException(error); } // Ex: Field is declared as an IIdType, but argument is an IdDt // or supported type conversion: String to ZonedDateTime - } else if (!theParameterClass.isAssignableFrom(methodParamClass) + } else if (!parameterType.isAssignableFrom(methodParamClass) && !optOperationEmbeddedParam .map(embeddedParam -> EmbeddedOperationUtils.isValidSourceTypeConversion( - methodParamClass, theParameterClass, embeddedParam.rangeType())) + methodParamClass, parameterType, embeddedParam.rangeType())) .orElse(false)) { final String error = String.format( "%sMismatch between methodParamClass: %s and parameterClassAtIndex: %s for method: %s", - Msg.code(236146125), methodParamClass, theParameterClass, myMethod.getName()); + Msg.code(236146125), methodParamClass, parameterType, myMethod.getName()); throw new InternalErrorException(error); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index fbec50948993..91373537b2e5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -82,7 +82,15 @@ static Constructor validateAndGetConstructor(Class theParameterTypeWithOpe return soleConstructor; } - // LUKETODO: javadoc + /** + * Checks to see if the constructor in question has any parameters that should be subject to a parameter conversion + * from a source type to a target type. This is currently only supported for String to ZonedDateTime. + * + * @param theMethodParamsWithoutRequestDetails method parameters without the request details + * @param theConstructorParameters constructor parameters for the embedded params class + * @param theAnnotations the annotations on the constructor params + * @return true if this is an expected source type conversion + */ static boolean hasAnyValidSourceTypeConversions( Object[] theMethodParamsWithoutRequestDetails, Parameter[] theConstructorParameters, @@ -99,34 +107,6 @@ static boolean hasAnyValidSourceTypeConversions( .anyMatch(Boolean::booleanValue); } - @Nonnull - private static Boolean isValidSourceTypeConversion( - Object[] theMethodParamsWithoutRequestDetails, - Parameter[] theConstructorParameters, - Annotation[] theAnnotations, - int theIndex) { - final Object methodParam = theMethodParamsWithoutRequestDetails[theIndex]; - - if (methodParam == null) { - return false; - } - - final Class methodParamClass = methodParam.getClass(); - final Class constructorParamType = theConstructorParameters[theIndex].getType(); - final Annotation annotation = theAnnotations[theIndex]; - - if (annotation instanceof OperationParam) { - final OperationParam operationParam = (OperationParam) annotation; - final OperationParameterRangeType operationParameterRangeType = operationParam.rangeType(); - - if (isValidSourceTypeConversion(methodParamClass, constructorParamType, operationParameterRangeType)) { - return true; - } - } - - return false; - } - /** * Indicate whether or not this is currently a supported type conversion * We currently only support converting from a String to a ZonedDateTime @@ -145,14 +125,18 @@ static boolean isValidSourceTypeConversion( && OperationParameterRangeType.NOT_APPLICABLE != theOperationParameterRangeType; } - public static List> getMethodParamsAnnotatedWithEmbeddableOperationParams(Method theMethod) { + static List> getMethodParamsAnnotatedWithEmbeddableOperationParams(Method theMethod) { return Arrays.stream(theMethod.getParameterTypes()) .filter(EmbeddedOperationUtils::hasEmbeddableOperationParamsAnnotation) .collect(Collectors.toUnmodifiableList()); } - private static boolean hasEmbeddableOperationParamsAnnotation(Class theMethodParameterType) { - final Annotation[] annotations = theMethodParameterType.getAnnotations(); + static boolean typeHasNoEmbeddableOperationParamsAnnotation(Class theType) { + return ! hasEmbeddableOperationParamsAnnotation(theType); + } + + private static boolean hasEmbeddableOperationParamsAnnotation(Class theType) { + final Annotation[] annotations = theType.getAnnotations(); if (annotations.length == 0) { return false; @@ -161,12 +145,40 @@ private static boolean hasEmbeddableOperationParamsAnnotation(Class theMethod if (annotations.length > 1) { throw new ConfigurationException(String.format( "%sInvalid operation embedded parameters. Class has more than one annotation: %s", - Msg.code(9132164), theMethodParameterType)); + Msg.code(9132164), theType)); } return EmbeddableOperationParams.class == annotations[0].annotationType(); } + @Nonnull + private static Boolean isValidSourceTypeConversion( + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations, + int theIndex) { + final Object methodParam = theMethodParamsWithoutRequestDetails[theIndex]; + + if (methodParam == null) { + return false; + } + + final Class methodParamClass = methodParam.getClass(); + final Class constructorParamType = theConstructorParameters[theIndex].getType(); + final Annotation annotation = theAnnotations[theIndex]; + + if (annotation instanceof OperationParam) { + final OperationParam operationParam = (OperationParam) annotation; + final OperationParameterRangeType operationParameterRangeType = operationParam.rangeType(); + + if (isValidSourceTypeConversion(methodParamClass, constructorParamType, operationParameterRangeType)) { + return true; + } + } + + return false; + } + private static void validateConstructorArgs(Constructor theConstructor, Field[] theDeclaredFields) { final Parameter[] constructorParameters = theConstructor.getParameters(); final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 9caa55c01423..f8741ec309c4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -31,7 +31,6 @@ import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Elements; -import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; @@ -71,16 +70,12 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -453,17 +448,10 @@ public Object outgoingClient(Object theObject) { + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } - // LUKETODO: refactor into some sort of static method - // LUKETODO: comment that this is a guard against adding the entire embeddable parameter as an - // OperationParameter - // LUKETODO: find a better guard for this? - - // LUKETODO: this doesn't work because the contexts are not empty - if (paramContexts.isEmpty() - || Arrays.stream(parameterType.getAnnotations()) - .noneMatch(annotation -> annotation instanceof EmbeddableOperationParams)) { - // RequestDetails if it's last + // Ensure that if we've processed embedded operations parameters and last parameter is a + // RequestDetails, we don't miss it + || EmbeddedOperationUtils.typeHasNoEmbeddableOperationParamsAnnotation(parameterType)) { paramContexts.add( new ParamInitializationContext(param, parameterType, outerCollectionType, innerCollectionType)); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 507210bedb75..3ff28cce78d3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -480,7 +480,6 @@ private OperationIdParamDetails getIdParamAnnotationFromMethodParams(Method theM } private Optional findIdParam(Method theMethod, int theParamIndex) { - // LUKETODO: do we need to validate there's only one? return Arrays.stream(theMethod.getParameterAnnotations()[theParamIndex]) .filter(IdParam.class::isInstance) .map(IdParam.class::cast) @@ -496,7 +495,6 @@ private OperationIdParamDetails findIdParamIndexForTypeWithEmbeddedParams( typeWithEmbeddedParams, RequestDetails.class, SystemRequestDetails.class)) { // skip } else { - // LUKETODO: fix final Constructor constructor = EmbeddedOperationUtils.validateAndGetConstructor(typeWithEmbeddedParams); diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 61cc0a1eb763..e8329e65e000 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -53,7 +53,6 @@ * myAdditionalData the data bundle containing additional data */ // LUKETODO: start to integrate this with a clinical reasoning branch -// LUKETODO: make code use or at least validate this annotation @EmbeddableOperationParams public class EvaluateMeasureSingleParams { private final IdType myId; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 63e4ee095d08..faac4a76678b 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -22,6 +22,7 @@ import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithoutAnnotations; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; @@ -35,7 +36,6 @@ // circular dependency class BaseMethodBindingMethodParameterBuilderTest { - // LUKETODO: test ZonedDateTime + IdParam // LUKETODO: assert Exception messages private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilderTest.class); @@ -93,7 +93,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { @Test void happyPathOperationEmbeddedTypesRequestDetailsFirst() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java index 2226c6a3f414..c3be528d0d2c 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -78,6 +78,25 @@ private static T unsafeCast(Object theObject) { } // Below are the methods and classed to test reflection code + public void methodWithNoAnnotations(String param) { + // Method implementation + } + + public void methodWithInvalidGenericType(List> param) { + // Method implementation + } + + public void methodWithUnknownTypeName(String param) { + // Method implementation + } + + public void methodWithNonAssignableTypeName(String param) { + // Method implementation + } + + public void methodWithInvalidAnnotation(String param) { + // Method implementation + } void superSimple() { } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 6e93580537b7..05283f5b2eab 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -10,7 +10,6 @@ import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; @@ -32,8 +31,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; -// LUKETODO: try to test for every case in embedded params where there's a throws - // This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency class MethodUtilTest { @@ -216,152 +213,39 @@ void paramsConversionIdTypeZonedDateTime() { } @Test - @Disabled - void getResourceParameters_withOptionalParam_shouldReturnSearchParameter() throws NoSuchMethodException { -// final Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); -// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new OptionalParam() { -// @Override -// public Class annotationType() { -// return OptionalParam.class; -// } -// -// @Override -// public String[] chainBlacklist() { -// return new String[0]; -// } -// -// @Override -// public String[] chainWhitelist() { -// return new String[0]; -// } -// -// @Override -// public Class[] compositeTypes() { -// return new Class[0]; -// } -// -// @Override -// public String name() { -// return "param"; -// } -// -// @Override -// public Class[] targetTypes() { -// return new Class[0]; -// } -// }}}); -// when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); -// -// List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); -// -// assertEquals(1, parameters.size()); -// assertInstanceOf(SearchParameter.class, parameters.get(0)); -// SearchParameter searchParameter = (SearchParameter) parameters.get(0); -// assertEquals("param", searchParameter.getName()); -// assertFalse(searchParameter.isRequired()); - } - - @Test - @Disabled - void getResourceParameters_withInvalidAnnotation_shouldThrowConfigurationException() throws NoSuchMethodException { -// Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); -// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{{new Annotation() { -// @Override -// public Class annotationType() { -// return Annotation.class; -// } -// }}}); -// when(method.getParameterTypes()).thenReturn(sampleMethod.getParameterTypes()); -// -// ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { -// MethodUtil.getResourceParameters(myFhirContext, method, provider); -// }); -// -// assertTrue(exception.getMessage().contains("has no recognized FHIR interface parameter nextParameterAnnotations")); - } - - @Test - @Disabled - void getResourceParameters_withMultipleAnnotations_shouldReturnCorrectParameters() throws NoSuchMethodException { -// Method sampleMethod = this.getClass().getDeclaredMethod("sampleMethod", String.class); -// when(method.getParameterAnnotations()).thenReturn(new Annotation[][]{ -// {new RequiredParam() { -// @Override -// public Class annotationType() { -// return RequiredParam.class; -// } -// -// @Override -// public String[] chainBlacklist() { -// return new String[0]; -// } -// -// @Override -// public String[] chainWhitelist() { -// return new String[0]; -// } -// -// @Override -// public Class[] compositeTypes() { -// return new Class[0]; -// } -// -// @Override -// public String name() { -// return "param1"; -// } -// -// @Override -// public Class[] targetTypes() { -// return new Class[0]; -// } -// }}, -// {new OptionalParam() { -// @Override -// public Class annotationType() { -// return OptionalParam.class; -// } -// -// @Override -// public String[] chainBlacklist() { -// return new String[0]; -// } -// -// @Override -// public String[] chainWhitelist() { -// return new String[0]; -// } -// -// @Override -// public Class[] compositeTypes() { -// return new Class[0]; -// } -// -// @Override -// public String name() { -// return "param2"; -// } -// -// @Override -// public Class[] targetTypes() { -// return new Class[0]; -// } -// }} -// }); -// when(method.getParameterTypes()).thenReturn(new Class[]{String.class, String.class}); -// -// List parameters = MethodUtil.getResourceParameters(myFhirContext, method, provider); -// -// assertEquals(2, parameters.size()); -// assertTrue(parameters.get(0) instanceof SearchParameter); -// assertTrue(parameters.get(1) instanceof SearchParameter); -// SearchParameter searchParameter1 = (SearchParameter) parameters.get(0); -// SearchParameter searchParameter2 = (SearchParameter) parameters.get(1); -// assertEquals("param1", searchParameter1.getName()); -// assertTrue(searchParameter1.isRequired()); -// assertEquals("param2", searchParameter2.getName()); -// assertFalse(searchParameter2.isRequired()); - } + void invalidMethodWithNoAnnotations() { + assertThatThrownBy(() -> getMethodAndExecute("methodWithNoAnnotations", String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); + } + + @Test + void invalidMethodWithInvalidGenericType() { + assertThatThrownBy(() -> getMethodAndExecute("methodWithInvalidGenericType", List.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("is of an invalid generic type"); + } + + @Test + void invalidMethodWithUnknownTypeName() { + assertThatThrownBy(() -> getMethodAndExecute("methodWithUnknownTypeName", String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); + } + + @Test + void invalidMethodWithNonAssignableTypeName() { + assertThatThrownBy(() -> getMethodAndExecute("methodWithNonAssignableTypeName", String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining(" has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); + } + + @Test + void invalidMethodWithInvalidAnnotation() { + assertThatThrownBy(() -> getMethodAndExecute("methodWithInvalidAnnotation", String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); + } private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( From 523158857ae72f9e2815837f40e6d647279fd76e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 11:18:56 -0500 Subject: [PATCH 61/75] Spotless. --- .../annotation/EmbeddableOperationParams.java | 1 - ...seMethodBindingMethodParameterBuilder.java | 17 ++++++----------- .../server/method/EmbeddedOperationUtils.java | 10 +++++----- .../method/EmbeddedParameterConverter.java | 19 +++++++++---------- 4 files changed, 20 insertions(+), 27 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java index efcf5d27efb7..0e725c87072a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/EmbeddableOperationParams.java @@ -24,7 +24,6 @@ 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index e97d686fde22..4fd5976bdfc4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -216,16 +216,14 @@ private Object[] convertParamsIfNeeded( } return IntStream.range(0, theMethodParamsWithoutRequestDetails.length) - .mapToObj(index -> convertParamIfNeeded( - theMethodParamsWithoutRequestDetails, theConstructorParameters, index)) + .mapToObj(index -> + convertParamIfNeeded(theMethodParamsWithoutRequestDetails, theConstructorParameters, index)) .toArray(Object[]::new); } @Nullable private Object convertParamIfNeeded( - Object[] theMethodParamsWithoutRequestDetails, - Parameter[] theConstructorParameters, - int theIndex) { + Object[] theMethodParamsWithoutRequestDetails, Parameter[] theConstructorParameters, int theIndex) { final Object paramAtIndex = theMethodParamsWithoutRequestDetails[theIndex]; @@ -324,9 +322,7 @@ private void validMethodParamTypes( } for (int index = 0; index < theMethodParamsWithoutRequestDetails.length; index++) { - validateMethodParamType( - theMethodParamsWithoutRequestDetails[index], - theConstructorParameters[index]); + validateMethodParamType(theMethodParamsWithoutRequestDetails[index], theConstructorParameters[index]); } } @@ -340,7 +336,7 @@ private void validateMethodParamType(Object theMethodParam, Parameter theConstru final Class methodParamClass = theMethodParam.getClass(); final Optional optOperationEmbeddedParam = - Optional.ofNullable(theConstructorParameter.getAnnotation(OperationParam.class)); + Optional.ofNullable(theConstructorParameter.getAnnotation(OperationParam.class)); optOperationEmbeddedParam.ifPresent(embeddedParam -> { if (embeddedParam.sourceType() != Void.class && methodParamClass != embeddedParam.sourceType()) { @@ -353,8 +349,7 @@ private void validateMethodParamType(Object theMethodParam, Parameter theConstru final Class parameterType = theConstructorParameter.getType(); - if (Collection.class.isAssignableFrom(methodParamClass) - || Collection.class.isAssignableFrom(parameterType)) { + if (Collection.class.isAssignableFrom(methodParamClass) || Collection.class.isAssignableFrom(parameterType)) { // ex: List and ArrayList if (methodParamClass.isAssignableFrom(parameterType)) { final String error = String.format( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 91373537b2e5..c7e9e3146bbf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -132,7 +132,7 @@ static List> getMethodParamsAnnotatedWithEmbeddableOperationParams(Meth } static boolean typeHasNoEmbeddableOperationParamsAnnotation(Class theType) { - return ! hasEmbeddableOperationParamsAnnotation(theType); + return !hasEmbeddableOperationParamsAnnotation(theType); } private static boolean hasEmbeddableOperationParamsAnnotation(Class theType) { @@ -153,10 +153,10 @@ private static boolean hasEmbeddableOperationParamsAnnotation(Class theType) @Nonnull private static Boolean isValidSourceTypeConversion( - Object[] theMethodParamsWithoutRequestDetails, - Parameter[] theConstructorParameters, - Annotation[] theAnnotations, - int theIndex) { + Object[] theMethodParamsWithoutRequestDetails, + Parameter[] theConstructorParameters, + Annotation[] theAnnotations, + int theIndex) { final Object methodParam = theMethodParamsWithoutRequestDetails[theIndex]; if (methodParam == null) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 6bf6a9e62844..2d6ad8ea340f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -54,31 +54,30 @@ public class EmbeddedParameterConverter { private final Class myOperationEmbeddedType; private final Constructor myConstructor; - public EmbeddedParameterConverter( - FhirContext theContext, Method theMethod, Operation theOperation) { + public EmbeddedParameterConverter(FhirContext theContext, Method theMethod, Operation theOperation) { if (theOperation == null) { throw new ConfigurationException(Msg.code(846192641) - + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " - + theMethod.toGenericString()); + + "@OperationParam or OperationEmbeddedParam detected on method that is not annotated with @Operation: " + + theMethod.toGenericString()); } final List> embeddedParamsClasses = Arrays.stream(theMethod.getParameterTypes()) - .filter(paramType -> paramType.isAnnotationPresent(EmbeddableOperationParams.class)) - .collect(Collectors.toUnmodifiableList()); + .filter(paramType -> paramType.isAnnotationPresent(EmbeddableOperationParams.class)) + .collect(Collectors.toUnmodifiableList()); // LUKETODO; better error? if (embeddedParamsClasses.isEmpty()) { throw new ConfigurationException(String.format( - "%sThere is no param with @EmbeddableOperationParams is supported for now for method: %s", - Msg.code(9999924), theMethod.getName())); + "%sThere is no param with @EmbeddableOperationParams is supported for now for method: %s", + Msg.code(9999924), theMethod.getName())); } // LUKETODO; better error? if (embeddedParamsClasses.size() > 1) { throw new ConfigurationException(String.format( - "%sMore than one param with with @EmbeddableOperationParams for method: %s", - Msg.code(9999927), theMethod.getName())); + "%sMore than one param with with @EmbeddableOperationParams for method: %s", + Msg.code(9999927), theMethod.getName())); } myContext = theContext; From 3c130653edf527260c6c61a64b8adffa7d5ce17b Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 12:01:25 -0500 Subject: [PATCH 62/75] Spotless. Partially refactor MethodUtil. Fix recently introduced bug. --- .../server/method/EmbeddedOperationUtils.java | 11 +- .../fhir/rest/server/method/MethodUtil.java | 669 +++++++++++------- .../method/EmbeddedOperationUtilsTest.java | 1 + 3 files changed, 437 insertions(+), 244 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index c7e9e3146bbf..03724d1b9f80 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -142,13 +142,22 @@ private static boolean hasEmbeddableOperationParamsAnnotation(Class theType) return false; } + final List embeddableOperationParams = Arrays.stream(annotations) + .filter(EmbeddableOperationParams.class::isInstance) + .map(EmbeddableOperationParams.class::cast) + .collect(Collectors.toUnmodifiableList()); + + if (embeddableOperationParams.isEmpty()) { + return false; + } + if (annotations.length > 1) { throw new ConfigurationException(String.format( "%sInvalid operation embedded parameters. Class has more than one annotation: %s", Msg.code(9132164), theType)); } - return EmbeddableOperationParams.class == annotations[0].annotationType(); + return true; } @Nonnull diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index f8741ec309c4..0adc49e32005 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -64,6 +64,8 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -100,19 +102,20 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] } } - @SuppressWarnings("unchecked") + + // LUKETODO: extract annotations method and make sure it works with embedded params public static List getResourceParameters( final FhirContext theContext, final Method theMethod, Object theProvider) { // We mutate this variable so distinguish this from the argument to getResourceParameters - Method methodToUse = theMethod; List parameters = new ArrayList<>(); - Class[] parameterTypes = methodToUse.getParameterTypes(); + Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; - for (Annotation[] nextParameterAnnotations : methodToUse.getParameterAnnotations()) { + for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { IParameter param = null; final List paramContexts = new ArrayList<>(); + Class declaredParameterType = parameterTypes[paramIndex]; Class parameterType = declaredParameterType; @@ -122,63 +125,13 @@ public static List getResourceParameters( // TagList is handled directly within the method bindings param = new NullParameter(); } else { - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); - if (parameterType == null && methodToUse.getDeclaringClass().isSynthetic()) { - try { - methodToUse = methodToUse - .getDeclaringClass() - .getSuperclass() - .getMethod(methodToUse.getName(), parameterTypes); - parameterType = - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); - } catch (NoSuchMethodException e) { - throw new ConfigurationException(Msg.code(400) + "A method with name '" - + methodToUse.getName() + "' does not exist for super class '" - + methodToUse.getDeclaringClass().getSuperclass() + "'"); - } - } - declaredParameterType = parameterType; - } + final GenericsContext genericsContext = + getGenericsContext(theContext, theMethod, parameterTypes, paramIndex); - if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); - declaredParameterType = parameterType; - } - if (parameterType == null || Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException( - Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() - + "' in type '" - + methodToUse.getDeclaringClass().getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); - } - - /* - * If the user is trying to bind IPrimitiveType they are probably - * trying to write code that is compatible across versions of FHIR. - * We'll try and come up with an appropriate subtype to give - * them. - * - * This gets tested in HistoryR4Test - */ - if (IPrimitiveType.class.equals(parameterType)) { - Class genericType = - ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); - if (Date.class.equals(genericType)) { - BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); - parameterType = Optional.ofNullable(dateTimeDef) - .map(BaseRuntimeElementDefinition::getImplementingClass) - .orElse(null); - } else if (String.class.equals(genericType) || genericType == null) { - BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); - parameterType = Optional.ofNullable(dateTimeDef) - .map(BaseRuntimeElementDefinition::getImplementingClass) - .orElse(null); - } - } + parameterType = genericsContext.getParameterType(); + declaredParameterType = genericsContext.getDeclaredParameterType(); + outerCollectionType = genericsContext.getOuterCollectionType(); + innerCollectionType = genericsContext.getInnerCollectionType(); } if (ServletRequest.class.isAssignableFrom(parameterType)) { @@ -199,88 +152,35 @@ public static List getResourceParameters( } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { - final Operation op = methodToUse.getAnnotation(Operation.class); + final Operation op = theMethod.getAnnotation(Operation.class); for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { Annotation nextAnnotation = nextParameterAnnotations[i]; if (nextAnnotation instanceof RequiredParam) { - SearchParameter parameter = new SearchParameter(); - parameter.setName(((RequiredParam) nextAnnotation).name()); - parameter.setRequired(true); - parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes()); - parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); - parameter.setChainLists( - ((RequiredParam) nextAnnotation).chainWhitelist(), - ((RequiredParam) nextAnnotation).chainBlacklist()); - parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, nextParameterAnnotations); - param = parameter; + param = createRequiredParam( + theContext, + nextParameterAnnotations, + (RequiredParam) nextAnnotation, + parameterType, + innerCollectionType, + outerCollectionType); } else if (nextAnnotation instanceof OptionalParam) { - SearchParameter parameter = new SearchParameter(); - parameter.setName(((OptionalParam) nextAnnotation).name()); - parameter.setRequired(false); - parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes()); - parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); - parameter.setChainLists( - ((OptionalParam) nextAnnotation).chainWhitelist(), - ((OptionalParam) nextAnnotation).chainBlacklist()); - parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, nextParameterAnnotations); - param = parameter; + param = createOptionalParam( + theContext, + nextParameterAnnotations, + (OptionalParam) nextAnnotation, + parameterType, + innerCollectionType, + outerCollectionType); } else if (nextAnnotation instanceof RawParam) { param = new RawParamsParameter(parameters); } else if (nextAnnotation instanceof IncludeParam) { - Class> instantiableCollectionType; - Class specType; - - if (parameterType == String.class) { - instantiableCollectionType = null; - specType = String.class; - } else if ((parameterType != Include.class) - || innerCollectionType == null - || outerCollectionType != null) { - throw new ConfigurationException(Msg.code(402) + "Method '" + methodToUse.getName() - + "' is annotated with @" + IncludeParam.class.getSimpleName() - + " but has a type other than Collection<" + Include.class.getSimpleName() + ">"); - } else { - instantiableCollectionType = (Class>) - CollectionBinder.getInstantiableCollectionType( - innerCollectionType, "Method '" + methodToUse.getName() + "'"); - specType = parameterType; - } - - param = new IncludeParameter( - (IncludeParam) nextAnnotation, instantiableCollectionType, specType); + param = createIncludeParam( + theMethod, parameterType, innerCollectionType, outerCollectionType, (IncludeParam) + nextAnnotation); } else if (nextAnnotation instanceof ResourceParam) { - Mode mode; - if (IBaseResource.class.isAssignableFrom(parameterType)) { - mode = Mode.RESOURCE; - } else if (String.class.equals(parameterType)) { - mode = ResourceParameter.Mode.BODY; - } else if (byte[].class.equals(parameterType)) { - mode = ResourceParameter.Mode.BODY_BYTE_ARRAY; - } else if (EncodingEnum.class.equals(parameterType)) { - mode = Mode.ENCODING; - } else { - StringBuilder b = new StringBuilder(); - b.append("Method '"); - b.append(methodToUse.getName()); - b.append("' is annotated with @"); - b.append(ResourceParam.class.getSimpleName()); - b.append(" but has a type that is not an implementation of "); - b.append(IBaseResource.class.getCanonicalName()); - b.append(" or String or byte[]"); - throw new ConfigurationException(Msg.code(403) + b.toString()); - } - boolean methodIsOperation = methodToUse.getAnnotation(Operation.class) != null; - boolean methodIsPatch = methodToUse.getAnnotation(Patch.class) != null; - param = new ResourceParameter( - (Class) parameterType, - theProvider, - mode, - methodIsOperation, - methodIsPatch); + param = createResourceParam(theMethod, theProvider, parameterType); } else if (nextAnnotation instanceof IdParam) { param = new NullParameter(); } else if (nextAnnotation instanceof ServerBase) { @@ -288,13 +188,10 @@ public static List getResourceParameters( } else if (nextAnnotation instanceof Elements) { param = new ElementsParameter(); } else if (nextAnnotation instanceof Since) { - param = new SinceParameter(); - ((SinceParameter) param) - .setType(theContext, parameterType, innerCollectionType, outerCollectionType); + param = createSinceParameter( + theContext, parameterType, innerCollectionType, outerCollectionType); } else if (nextAnnotation instanceof At) { - param = new AtParameter(); - ((AtParameter) param) - .setType(theContext, parameterType, innerCollectionType, outerCollectionType); + param = createAtParameter(theContext, parameterType, innerCollectionType, outerCollectionType); } else if (nextAnnotation instanceof Count) { param = new CountParameter(); } else if (nextAnnotation instanceof Offset) { @@ -310,44 +207,16 @@ public static List getResourceParameters( } else if (nextAnnotation instanceof ConditionalUrlParam) { param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); } else if (nextAnnotation instanceof OperationParam) { - if (op == null) { - throw new ConfigurationException(Msg.code(404) - + "@OperationParam detected on method that is not annotated with @Operation: " - + methodToUse.toGenericString()); - } - - OperationParam operationParam = (OperationParam) nextAnnotation; - String description = ParametersUtil.extractDescription(nextParameterAnnotations); - List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - - param = new OperationParameter( + final ParameterContext paramContext = createOperationParameterContext( theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples, - operationParam.sourceType(), - operationParam.rangeType()); - if (isNotBlank(operationParam.typeName())) { - BaseRuntimeElementDefinition elementDefinition = - theContext.getElementDefinition(operationParam.typeName()); - if (elementDefinition == null) { - elementDefinition = theContext.getResourceDefinition(operationParam.typeName()); - } - org.apache.commons.lang3.Validate.notNull( - elementDefinition, - "Unknown type name in @OperationParam: typeName=\"%s\"", - operationParam.typeName()); - - Class newParameterType = elementDefinition.getImplementingClass(); - if (!declaredParameterType.isAssignableFrom(newParameterType)) { - throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" - + operationParam.typeName() + "\" specified on method " + methodToUse); - } - parameterType = newParameterType; - } + theMethod, + nextParameterAnnotations, + nextAnnotation, + parameterType, + declaredParameterType); + + param = paramContext.getParam(); + parameterType = paramContext.getParameterType(); } else if (nextAnnotation instanceof EmbeddedOperationParams) { final EmbeddedParameterConverter embeddedParameterConverter = new EmbeddedParameterConverter(theContext, theMethod, op); @@ -368,73 +237,10 @@ public static List getResourceParameters( } } } else if (nextAnnotation instanceof Validate.Mode) { - if (!parameterType.equals(ValidationModeEnum.class)) { - throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" - + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() - + " must be of type " + ValidationModeEnum.class.getName()); - } - String description = ParametersUtil.extractDescription(nextParameterAnnotations); - List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_MODE, - 0, - 1, - description, - examples, - Void.class, - OperationParameterRangeType.NOT_APPLICABLE) - .setConverter(new IOperationParamConverter() { - @Override - public Object incomingServer(Object theObject) { - if (isNotBlank(theObject.toString())) { - ValidationModeEnum retVal = - ValidationModeEnum.forCode(theObject.toString()); - if (retVal == null) { - OperationParameter.throwInvalidMode(theObject.toString()); - } - return retVal; - } - return null; - } - - @Override - public Object outgoingClient(Object theObject) { - return ParametersUtil.createString( - theContext, ((ValidationModeEnum) theObject).getCode()); - } - }); + param = createValidateNode(theContext, nextParameterAnnotations, parameterType); } else { if (nextAnnotation instanceof Validate.Profile) { - if (!parameterType.equals(String.class)) { - throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" - + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() - + " must be of type " + String.class.getName()); - } - String description = ParametersUtil.extractDescription(nextParameterAnnotations); - List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - param = new OperationParameter( - theContext, - Constants.EXTOP_VALIDATE, - Constants.EXTOP_VALIDATE_PROFILE, - 0, - 1, - description, - examples, - Void.class, - OperationParameterRangeType.NOT_APPLICABLE) - .setConverter(new IOperationParamConverter() { - @Override - public Object incomingServer(Object theObject) { - return theObject.toString(); - } - - @Override - public Object outgoingClient(Object theObject) { - return ParametersUtil.createString(theContext, theObject.toString()); - } - }); + param = createValidateProfile(theContext, nextParameterAnnotations, parameterType); } } } @@ -443,8 +249,8 @@ public Object outgoingClient(Object theObject) { if (param == null) { throw new ConfigurationException( Msg.code(408) + "Parameter #" + (paramIndex + 1) + "/" + (parameterTypes.length) - + " of method '" + methodToUse.getName() + "' on type '" - + methodToUse.getDeclaringClass().getCanonicalName() + + " of method '" + theMethod.getName() + "' on type '" + + theMethod.getDeclaringClass().getCanonicalName() + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } @@ -457,7 +263,7 @@ public Object outgoingClient(Object theObject) { } for (ParamInitializationContext paramContext : paramContexts) { - paramContext.initialize(methodToUse); + paramContext.initialize(theMethod); parameters.add(paramContext.getParam()); } @@ -465,4 +271,381 @@ public Object outgoingClient(Object theObject) { } return parameters; } + + @Nonnull + private static IParameter createAtParameter( + FhirContext theContext, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + final AtParameter param = new AtParameter(); + param.setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + return param; + } + + @Nonnull + private static IParameter createSinceParameter( + FhirContext theTheContext, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + final SinceParameter param = new SinceParameter(); + param.setType(theTheContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + return param; + } + + @Nonnull + private static IParameter createRequiredParam( + FhirContext theContext, + Annotation[] theNextParameterAnnotations, + RequiredParam theNextAnnotation, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + IParameter param; + SearchParameter parameter = new SearchParameter(); + parameter.setName(theNextAnnotation.name()); + parameter.setRequired(true); + parameter.setDeclaredTypes(theNextAnnotation.targetTypes()); + parameter.setCompositeTypes(theNextAnnotation.compositeTypes()); + parameter.setChainLists(theNextAnnotation.chainWhitelist(), theNextAnnotation.chainBlacklist()); + parameter.setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + MethodUtil.extractDescription(parameter, theNextParameterAnnotations); + param = parameter; + return param; + } + + @Nonnull + private static IParameter createOptionalParam( + FhirContext theContext, + Annotation[] theNextParameterAnnotations, + OptionalParam theNextAnnotation, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + IParameter param; + SearchParameter parameter = new SearchParameter(); + parameter.setName(theNextAnnotation.name()); + parameter.setRequired(false); + parameter.setDeclaredTypes(theNextAnnotation.targetTypes()); + parameter.setCompositeTypes(theNextAnnotation.compositeTypes()); + parameter.setChainLists(theNextAnnotation.chainWhitelist(), theNextAnnotation.chainBlacklist()); + parameter.setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + MethodUtil.extractDescription(parameter, theNextParameterAnnotations); + param = parameter; + return param; + } + + @Nonnull + private static IParameter createIncludeParam( + Method theMethod, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType, + IncludeParam theNextAnnotation) { + IParameter param; + Class> instantiableCollectionType; + Class specType; + + if (theParameterType == String.class) { + instantiableCollectionType = null; + specType = String.class; + } else if ((theParameterType != Include.class) + || theInnerCollectionType == null + || theOuterCollectionType != null) { + throw new ConfigurationException(Msg.code(402) + "Method '" + theMethod.getName() + + "' is annotated with @" + IncludeParam.class.getSimpleName() + + " but has a type other than Collection<" + Include.class.getSimpleName() + ">"); + } else { + instantiableCollectionType = unsafeCast(CollectionBinder.getInstantiableCollectionType( + theInnerCollectionType, "Method '" + theMethod.getName() + "'")); + specType = theParameterType; + } + + param = new IncludeParameter(theNextAnnotation, instantiableCollectionType, specType); + return param; + } + + @Nonnull + private static IParameter createResourceParam(Method theMethod, Object theProvider, Class theParameterType) { + IParameter param; + Mode mode; + if (IBaseResource.class.isAssignableFrom(theParameterType)) { + mode = Mode.RESOURCE; + } else if (String.class.equals(theParameterType)) { + mode = Mode.BODY; + } else if (byte[].class.equals(theParameterType)) { + mode = Mode.BODY_BYTE_ARRAY; + } else if (EncodingEnum.class.equals(theParameterType)) { + mode = Mode.ENCODING; + } else { + final String error = String.format( + "%sMethod: '%s' is annotated with @%s but has a type that is not an implementation of %s or String or byte[]", + Msg.code(403), + theMethod.getName(), + ResourceParam.class.getSimpleName(), + IBaseResource.class.getCanonicalName()); + throw new ConfigurationException(error); + } + boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; + boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null; + param = new ResourceParameter( + unsafeCast(theParameterType), theProvider, mode, methodIsOperation, methodIsPatch); + return param; + } + + private static IParameter createValidateNode( + FhirContext theContext, Annotation[] theNextParameterAnnotations, Class theParameterType) { + IParameter param; + if (!theParameterType.equals(ValidationModeEnum.class)) { + throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" + + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + + " must be of type " + ValidationModeEnum.class.getName()); + } + String description = ParametersUtil.extractDescription(theNextParameterAnnotations); + List examples = ParametersUtil.extractExamples(theNextParameterAnnotations); + param = new OperationParameter( + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_MODE, + 0, + 1, + description, + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) + .setConverter(new IOperationParamConverter() { + @Override + public Object incomingServer(Object theObject) { + if (isNotBlank(theObject.toString())) { + ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString()); + if (retVal == null) { + OperationParameter.throwInvalidMode(theObject.toString()); + } + return retVal; + } + return null; + } + + @Override + public Object outgoingClient(Object theObject) { + return ParametersUtil.createString(theContext, ((ValidationModeEnum) theObject).getCode()); + } + }); + return param; + } + + private static IParameter createValidateProfile( + FhirContext theContext, Annotation[] theNextParameterAnnotations, Class theParameterType) { + IParameter param; + if (!theParameterType.equals(String.class)) { + throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" + + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + + " must be of type " + String.class.getName()); + } + String description = ParametersUtil.extractDescription(theNextParameterAnnotations); + List examples = ParametersUtil.extractExamples(theNextParameterAnnotations); + param = new OperationParameter( + theContext, + Constants.EXTOP_VALIDATE, + Constants.EXTOP_VALIDATE_PROFILE, + 0, + 1, + description, + examples, + Void.class, + OperationParameterRangeType.NOT_APPLICABLE) + .setConverter(new IOperationParamConverter() { + @Override + public Object incomingServer(Object theObject) { + return theObject.toString(); + } + + @Override + public Object outgoingClient(Object theObject) { + return ParametersUtil.createString(theContext, theObject.toString()); + } + }); + return param; + } + + @Nonnull + private static ParameterContext createOperationParameterContext( + FhirContext theContext, + Method theMethod, + Annotation[] theNextParameterAnnotations, + Annotation theNextAnnotation, + Class theParameterType, + Class theDeclaredParameterType) { + final Operation op = theMethod.getAnnotation(Operation.class); + if (op == null) { + throw new ConfigurationException(Msg.code(404) + + "@OperationParam detected on method that is not annotated with @Operation: " + + theMethod.toGenericString()); + } + + final OperationParam operationParam = (OperationParam) theNextAnnotation; + final String description = ParametersUtil.extractDescription(theNextParameterAnnotations); + final List examples = ParametersUtil.extractExamples(theNextParameterAnnotations); + Class parameterTypeInner = theParameterType; + + final OperationParameter param = new OperationParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples, + operationParam.sourceType(), + operationParam.rangeType()); + + if (isNotBlank(operationParam.typeName())) { + BaseRuntimeElementDefinition elementDefinition = + theContext.getElementDefinition(operationParam.typeName()); + if (elementDefinition == null) { + elementDefinition = theContext.getResourceDefinition(operationParam.typeName()); + } + org.apache.commons.lang3.Validate.notNull( + elementDefinition, + "Unknown type name in @OperationParam: typeName=\"%s\"", + operationParam.typeName()); + + Class newParameterType = elementDefinition.getImplementingClass(); + if (!theDeclaredParameterType.isAssignableFrom(newParameterType)) { + throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + + operationParam.typeName() + "\" specified on method " + theMethod); + } + parameterTypeInner = newParameterType; + } + + return new ParameterContext(parameterTypeInner, param); + } + + private static GenericsContext getGenericsContext( + FhirContext theContext, Method theMethod, Class[] theParameterTypes, int theParamIndex) { + + Class declaredParameterType = theParameterTypes[theParamIndex]; + Class parameterType = declaredParameterType; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = unsafeCast(parameterType); + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); + if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { + try { + theMethod = theMethod + .getDeclaringClass() + .getSuperclass() + .getMethod(theMethod.getName(), theParameterTypes); + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(Msg.code(400) + "A method with name '" + + theMethod.getName() + "' does not exist for super class '" + + theMethod.getDeclaringClass().getSuperclass() + "'"); + } + } + declaredParameterType = parameterType; + } + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = unsafeCast(parameterType); + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); + declaredParameterType = parameterType; + } + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException( + Msg.code(401) + "Argument #" + theParamIndex + " of Method '" + theMethod.getName() + + "' in type '" + + theMethod.getDeclaringClass().getCanonicalName() + + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + + /* + * If the user is trying to bind IPrimitiveType they are probably + * trying to write code that is compatible across versions of FHIR. + * We'll try and come up with an appropriate subtype to give + * them. + * + * This gets tested in HistoryR4Test + */ + if (IPrimitiveType.class.equals(parameterType)) { + Class genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); + if (Date.class.equals(genericType)) { + BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); + parameterType = Optional.ofNullable(dateTimeDef) + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); + } else if (String.class.equals(genericType) || genericType == null) { + BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); + parameterType = Optional.ofNullable(dateTimeDef) + .map(BaseRuntimeElementDefinition::getImplementingClass) + .orElse(null); + } + } + + return new GenericsContext(parameterType, declaredParameterType, outerCollectionType, innerCollectionType); + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T) theObject; + } + + // LUKETODO: top level? + private static class GenericsContext { + private final Class parameterType; + private final Class declaredParameterType; + private final Class> outerCollectionType; + private final Class> innerCollectionType; + + public GenericsContext( + Class theParameterType, + Class theDeclaredParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType) { + parameterType = theParameterType; + declaredParameterType = theDeclaredParameterType; + outerCollectionType = theOuterCollectionType; + innerCollectionType = theInnerCollectionType; + } + + public Class getParameterType() { + return parameterType; + } + + public Class getDeclaredParameterType() { + return declaredParameterType; + } + + public Class> getOuterCollectionType() { + return outerCollectionType; + } + + public Class> getInnerCollectionType() { + return innerCollectionType; + } + } + + // LUKETODO: refactor to use only one of the Context classes + private static class ParameterContext { + private final Class myParameterType; + + @Nullable + private final IParameter myParameter; + + public ParameterContext(Class theParameterType, @Nullable IParameter theParameter) { + myParameter = theParameter; + myParameterType = theParameterType; + } + + public IParameter getParam() { + return myParameter; + } + + public Class getParameterType() { + return myParameterType; + } + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java index 3cca93ba5b2b..91110963741a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtilsTest.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +// LUKETODO: test for non-embedded case where we have multiple annotations class EmbeddedOperationUtilsTest { @EmbeddableOperationParams private static class SimpleFieldsAndConstructorInOrder { From 494f1d0fe6ac89ab490ee6a93b81b6eac60883ee Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 12:03:36 -0500 Subject: [PATCH 63/75] Spotless. --- .../uhn/fhir/rest/server/method/EmbeddedOperationUtils.java | 6 +++--- .../java/ca/uhn/fhir/rest/server/method/MethodUtil.java | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 03724d1b9f80..e9d19a088f3b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -143,9 +143,9 @@ private static boolean hasEmbeddableOperationParamsAnnotation(Class theType) } final List embeddableOperationParams = Arrays.stream(annotations) - .filter(EmbeddableOperationParams.class::isInstance) - .map(EmbeddableOperationParams.class::cast) - .collect(Collectors.toUnmodifiableList()); + .filter(EmbeddableOperationParams.class::isInstance) + .map(EmbeddableOperationParams.class::cast) + .collect(Collectors.toUnmodifiableList()); if (embeddableOperationParams.isEmpty()) { return false; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 0adc49e32005..9acc83016958 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -102,7 +102,6 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] } } - // LUKETODO: extract annotations method and make sure it works with embedded params public static List getResourceParameters( final FhirContext theContext, final Method theMethod, Object theProvider) { From b44ee9d6cc58c10c7cd5dc98d925f16bc3d3bcf2 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 13:53:14 -0500 Subject: [PATCH 64/75] Simplify implementation even more. Support a separate method to do non-operation parameter and non-embedded parameter annotations. --- .../fhir/rest/server/method/MethodUtil.java | 219 +++++++++++------- .../method/ParamInitializationContext.java | 4 + 2 files changed, 135 insertions(+), 88 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 9acc83016958..15072c03c574 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -151,96 +151,69 @@ public static List getResourceParameters( } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { - final Operation op = theMethod.getAnnotation(Operation.class); - for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { - Annotation nextAnnotation = nextParameterAnnotations[i]; - - if (nextAnnotation instanceof RequiredParam) { - param = createRequiredParam( - theContext, - nextParameterAnnotations, - (RequiredParam) nextAnnotation, - parameterType, - innerCollectionType, - outerCollectionType); - } else if (nextAnnotation instanceof OptionalParam) { - param = createOptionalParam( - theContext, - nextParameterAnnotations, - (OptionalParam) nextAnnotation, - parameterType, - innerCollectionType, - outerCollectionType); - } else if (nextAnnotation instanceof RawParam) { - param = new RawParamsParameter(parameters); - } else if (nextAnnotation instanceof IncludeParam) { - param = createIncludeParam( - theMethod, parameterType, innerCollectionType, outerCollectionType, (IncludeParam) - nextAnnotation); - } else if (nextAnnotation instanceof ResourceParam) { - param = createResourceParam(theMethod, theProvider, parameterType); - } else if (nextAnnotation instanceof IdParam) { - param = new NullParameter(); - } else if (nextAnnotation instanceof ServerBase) { - param = new ServerBaseParamBinder(); - } else if (nextAnnotation instanceof Elements) { - param = new ElementsParameter(); - } else if (nextAnnotation instanceof Since) { - param = createSinceParameter( - theContext, parameterType, innerCollectionType, outerCollectionType); - } else if (nextAnnotation instanceof At) { - param = createAtParameter(theContext, parameterType, innerCollectionType, outerCollectionType); - } else if (nextAnnotation instanceof Count) { - param = new CountParameter(); - } else if (nextAnnotation instanceof Offset) { - param = new OffsetParameter(); - } else if (nextAnnotation instanceof GraphQLQueryUrl) { - param = new GraphQLQueryUrlParameter(); - } else if (nextAnnotation instanceof GraphQLQueryBody) { - param = new GraphQLQueryBodyParameter(); - } else if (nextAnnotation instanceof Sort) { - param = new SortParameter(theContext); - } else if (nextAnnotation instanceof TransactionParam) { - param = new TransactionParameter(theContext); - } else if (nextAnnotation instanceof ConditionalUrlParam) { - param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple()); - } else if (nextAnnotation instanceof OperationParam) { - final ParameterContext paramContext = createOperationParameterContext( - theContext, - theMethod, - nextParameterAnnotations, - nextAnnotation, - parameterType, - declaredParameterType); - - param = paramContext.getParam(); - parameterType = paramContext.getParameterType(); - } else if (nextAnnotation instanceof EmbeddedOperationParams) { - final EmbeddedParameterConverter embeddedParameterConverter = - new EmbeddedParameterConverter(theContext, theMethod, op); - - for (EmbeddedParameterConverterContext outerContext : embeddedParameterConverter.convert()) { - if (outerContext.getParameter() != null) { - parameters.add(outerContext.getParameter()); - } + final Annotation nextParameterAnnotation = nextParameterAnnotations[i]; + + final IParameter paramForNonOperationNonEmbeddedAnnotation = + getParamForNonOperationNonEmbeddedAnnotation( + theContext, + theProvider, + theMethod, + nextParameterAnnotations, + nextParameterAnnotation, + parameterType, + outerCollectionType, + innerCollectionType, + parameters); + + if (paramForNonOperationNonEmbeddedAnnotation != null) { + param = paramForNonOperationNonEmbeddedAnnotation; + } else if ((nextParameterAnnotation instanceof OperationParam) + || (nextParameterAnnotation instanceof EmbeddedOperationParams)) { + final Operation op = theMethod.getAnnotation(Operation.class); + + if (op == null) { + throw new ConfigurationException(Msg.code(404) + + "@OperationParam or @EmbeddedOperationParams detected on method that is not annotated with @Operation: " + + theMethod.toGenericString()); + } + + if (nextParameterAnnotation instanceof OperationParam) { + final ParamInitializationContext operationParamContext = createOperationParamContext( + theContext, + theMethod, + nextParameterAnnotations, + nextParameterAnnotation, + parameterType, + declaredParameterType, + outerCollectionType, + innerCollectionType); + + param = operationParamContext.getParam(); + parameterType = operationParamContext.getParameterType(); + } - final ParamInitializationContext paramContext = outerContext.getParamContext(); + if (nextParameterAnnotation instanceof EmbeddedOperationParams) { + final EmbeddedParameterConverter embeddedParameterConverter = + new EmbeddedParameterConverter(theContext, theMethod, op); - if (paramContext != null) { - paramContexts.add(paramContext); + for (EmbeddedParameterConverterContext outerContext : + embeddedParameterConverter.convert()) { + if (outerContext.getParameter() != null) { + parameters.add(outerContext.getParameter()); + } - // N.B. This a hack used only to pass the null check below, which is crucial to the - // non-embedded params logic - param = paramContext.getParam(); + final ParamInitializationContext paramContext = outerContext.getParamContext(); + + if (paramContext != null) { + paramContexts.add(paramContext); + + // N.B. This a hack used only to pass the null check below, which is crucial to the + // non-embedded params logic + param = paramContext.getParam(); + } } - } - } else if (nextAnnotation instanceof Validate.Mode) { - param = createValidateNode(theContext, nextParameterAnnotations, parameterType); - } else { - if (nextAnnotation instanceof Validate.Profile) { - param = createValidateProfile(theContext, nextParameterAnnotations, parameterType); - } + } // else param is null, and we throw, assuming nothing next in the loop doesn't this variable } } } @@ -271,6 +244,73 @@ public static List getResourceParameters( return parameters; } + @Nullable + private static IParameter getParamForNonOperationNonEmbeddedAnnotation( + FhirContext theContext, + Object theProvider, + Method theMethod, + Annotation[] theNextParameterAnnotations, + Annotation theNextAnnotation, + Class theParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType, + List theParameters) { + if (theNextAnnotation instanceof RequiredParam) { + return createRequiredParam( + theContext, + theNextParameterAnnotations, + (RequiredParam) theNextAnnotation, + theParameterType, + theInnerCollectionType, + theOuterCollectionType); + } else if (theNextAnnotation instanceof OptionalParam) { + return createOptionalParam( + theContext, + theNextParameterAnnotations, + (OptionalParam) theNextAnnotation, + theParameterType, + theInnerCollectionType, + theOuterCollectionType); + } else if (theNextAnnotation instanceof RawParam) { + return new RawParamsParameter(theParameters); + } else if (theNextAnnotation instanceof IncludeParam) { + return createIncludeParam( + theMethod, theParameterType, theInnerCollectionType, theOuterCollectionType, (IncludeParam) + theNextAnnotation); + } else if (theNextAnnotation instanceof ResourceParam) { + return createResourceParam(theMethod, theProvider, theParameterType); + } else if (theNextAnnotation instanceof IdParam) { + return new NullParameter(); + } else if (theNextAnnotation instanceof ServerBase) { + return new ServerBaseParamBinder(); + } else if (theNextAnnotation instanceof Elements) { + return new ElementsParameter(); + } else if (theNextAnnotation instanceof Since) { + return createSinceParameter(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + } else if (theNextAnnotation instanceof At) { + return createAtParameter(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + } else if (theNextAnnotation instanceof Count) { + return new CountParameter(); + } else if (theNextAnnotation instanceof Offset) { + return new OffsetParameter(); + } else if (theNextAnnotation instanceof GraphQLQueryUrl) { + return new GraphQLQueryUrlParameter(); + } else if (theNextAnnotation instanceof GraphQLQueryBody) { + return new GraphQLQueryBodyParameter(); + } else if (theNextAnnotation instanceof Sort) { + return new SortParameter(theContext); + } else if (theNextAnnotation instanceof TransactionParam) { + return new TransactionParameter(theContext); + } else if (theNextAnnotation instanceof ConditionalUrlParam) { + return new ConditionalParamBinder(((ConditionalUrlParam) theNextAnnotation).supportsMultiple()); + } else if (theNextAnnotation instanceof Validate.Mode) { + return createValidateNode(theContext, theNextParameterAnnotations, theParameterType); + } else if (theNextAnnotation instanceof Validate.Profile) { + return createValidateProfile(theContext, theNextParameterAnnotations, theParameterType); + } + return null; + } + @Nonnull private static IParameter createAtParameter( FhirContext theContext, @@ -469,13 +509,15 @@ public Object outgoingClient(Object theObject) { } @Nonnull - private static ParameterContext createOperationParameterContext( + private static ParamInitializationContext createOperationParamContext( FhirContext theContext, Method theMethod, Annotation[] theNextParameterAnnotations, Annotation theNextAnnotation, Class theParameterType, - Class theDeclaredParameterType) { + Class theDeclaredParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType) { final Operation op = theMethod.getAnnotation(Operation.class); if (op == null) { throw new ConfigurationException(Msg.code(404) @@ -518,7 +560,8 @@ private static ParameterContext createOperationParameterContext( parameterTypeInner = newParameterType; } - return new ParameterContext(parameterTypeInner, param); + return new ParamInitializationContext( + param, parameterTypeInner, theOuterCollectionType, theInnerCollectionType); } private static GenericsContext getGenericsContext( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java index 0b4a1e91957d..32b6ae9cf6eb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ParamInitializationContext.java @@ -46,6 +46,10 @@ public IParameter getParam() { return myParam; } + public Class getParameterType() { + return myParameterType; + } + void initialize(Method theMethod) { myParam.initializeTypes(theMethod, myOuterCollectionType, myInnerCollectionType, myParameterType); } From ba41e496bab541061b2e6d58908c81a1d9aa3e5e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 16:33:31 -0500 Subject: [PATCH 65/75] Fix error message and assertion. --- .../java/ca/uhn/fhir/rest/server/method/MethodUtil.java | 8 +++++--- .../fhir/rest/server/ServerInvalidDefinitionR4Test.java | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 15072c03c574..6648d369688d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -173,9 +173,11 @@ public static List getResourceParameters( final Operation op = theMethod.getAnnotation(Operation.class); if (op == null) { - throw new ConfigurationException(Msg.code(404) - + "@OperationParam or @EmbeddedOperationParams detected on method that is not annotated with @Operation: " - + theMethod.toGenericString()); + final String error = String.format("%s@OperationParam or @EmbeddedOperationParams detected on method: [%s] that is not annotated with @Operation: %s", + Msg.code(404), + theMethod.getName(), + theMethod.toGenericString()); + throw new ConfigurationException(error); } if (nextParameterAnnotation instanceof OperationParam) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java index c2ac742e06c8..3d2cc64f7dc9 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java @@ -116,7 +116,7 @@ public List search( try { startServer(provider); fail(); } catch (ConfigurationException e) { - assertEquals(Msg.code(288) + "Failure scanning class MyProvider: " + Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: public java.util.List ca.uhn.fhir.rest.server.ServerInvalidDefinitionR4Test$2MyProvider.search(org.hl7.fhir.r4.model.StringType,org.hl7.fhir.r4.model.StringType)", e.getMessage()); + assertEquals(Msg.code(288) + "Failure scanning class MyProvider: " + Msg.code(404) + "@OperationParam or @EmbeddedOperationParams detected on method: [search] that is not annotated with @Operation: public java.util.List ca.uhn.fhir.rest.server.ServerInvalidDefinitionR4Test$2MyProvider.search(org.hl7.fhir.r4.model.StringType,org.hl7.fhir.r4.model.StringType)", e.getMessage()); } } From b823a1efa85bb9c1f128c32d9ab0ce849c9220a2 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 16:34:57 -0500 Subject: [PATCH 66/75] Spotless. --- .../java/ca/uhn/fhir/rest/server/method/MethodUtil.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 6648d369688d..a88edf3c011d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -173,10 +173,9 @@ public static List getResourceParameters( final Operation op = theMethod.getAnnotation(Operation.class); if (op == null) { - final String error = String.format("%s@OperationParam or @EmbeddedOperationParams detected on method: [%s] that is not annotated with @Operation: %s", - Msg.code(404), - theMethod.getName(), - theMethod.toGenericString()); + final String error = String.format( + "%s@OperationParam or @EmbeddedOperationParams detected on method: [%s] that is not annotated with @Operation: %s", + Msg.code(404), theMethod.getName(), theMethod.toGenericString()); throw new ConfigurationException(error); } From 507850af9cbe71d8ecbf3f9142f495c57f5c44eb Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 19:56:04 -0500 Subject: [PATCH 67/75] Remove logging to address PHI errors. --- ...seMethodBindingMethodParameterBuilder.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 4fd5976bdfc4..bad157c177e8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -28,8 +28,6 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -53,8 +51,6 @@ */ class BaseMethodBindingMethodParameterBuilder { - private static final Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilder.class); - private final StringTimePeriodHandler myStringTimePeriodHandler = new StringTimePeriodHandler(ZoneOffset.UTC); private final Method myMethod; @@ -96,12 +92,6 @@ private Object[] tryBuildMethodParams() Msg.code(234198927), myMethod, Arrays.toString(myInputMethodParams))); } - ourLog.info( - "1234: START building for method: {}, requestDetails: {}, inputMethodParams: {}", - myMethod.getName(), - myRequestDetails, - Arrays.toString(myInputMethodParams)); - final List> parameterTypesWithOperationEmbeddedParams = EmbeddedOperationUtils.getMethodParamsAnnotatedWithEmbeddableOperationParams(myMethod); @@ -152,30 +142,10 @@ private Object[] determineMethodParamsForOperationEmbeddedParams( Class theParameterTypeWithOperationEmbeddedParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { - final String methodName = myMethod.getName(); - - ourLog.info( - "1234: invoking parameterTypeWithOperationEmbeddedParams: {} and theMethod: {}", - theParameterTypeWithOperationEmbeddedParams, - methodName); - final Object operationEmbeddedType = buildOperationEmbeddedObject(theParameterTypeWithOperationEmbeddedParams, myInputMethodParams); - ourLog.info( - "1234: build method params with embedded object and requestDetails (if applicable) for: {}", - operationEmbeddedType); - - final Object[] params = buildMethodParamsInCorrectPositions(operationEmbeddedType); - - ourLog.info( - "1234: END: method: {}, requestDetails: {}, inputMethodParams: {}, outputMethodParams: {}", - myMethod.getName(), - myRequestDetails, - Arrays.toString(myInputMethodParams), - Arrays.toString(params)); - - return params; + return buildMethodParamsInCorrectPositions(operationEmbeddedType); } @Nonnull From c548b2cc0712d580c25c268cfb53e66a61ca7e69 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 27 Jan 2025 09:23:51 -0500 Subject: [PATCH 68/75] Fix tests. --- .../rest/server/method/MethodUtilTest.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 05283f5b2eab..c7571e91acf0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -69,9 +69,9 @@ void sampleMethodOperationParams() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_OPERATION_PARAMS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -89,9 +89,9 @@ void sampleMethodOperationParamsWithFhirTypes() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class,null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_OPERATION_PARAMS, ArrayList.class, String.class,null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -127,8 +127,8 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST,ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -145,8 +145,8 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), new RequestDetailsParameterToAssert() ); @@ -165,9 +165,9 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), - new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); assertThat(resourceParameters) @@ -203,8 +203,8 @@ void paramsConversionIdTypeZonedDateTime() { final List expectedParameters = List.of( new NullParameterToAssert(), - new OperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), - new OperationParameterToAssert(ourFhirContext, "periodEnd", SIMPLE_METHOD_WITH_PARAMS_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) + new OperationParameterToAssert(ourFhirContext, "periodStart", SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), + new OperationParameterToAssert(ourFhirContext, "periodEnd", SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION, null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.END) ); assertThat(resourceParameters) @@ -282,6 +282,7 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I } if (theExpectedParameter instanceof OperationParameterToAssert expectedOperationParameter && theActualParameter instanceof OperationParameter actualOperationParameter) { + assertThat(actualOperationParameter.getOperationName()).isEqualTo(expectedOperationParameter.myOperationName()); assertThat(actualOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationParameter.myContext().getVersion().getVersion()); assertThat(actualOperationParameter.getName()).isEqualTo(expectedOperationParameter.myName()); assertThat(actualOperationParameter.getParamType()).isEqualTo(expectedOperationParameter.myParamType()); From 2fba720adcdbdb70b2ecee0975974e41ff6c2b93 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 28 Jan 2025 15:19:48 -0500 Subject: [PATCH 69/75] Rename common method reflection test class. Add all MethodUtilTest methods from the methodutil branch. Branch out MethodUtil generics context into an outer class. --- .../fhir/rest/server/method/MethodUtil.java | 64 +- .../method/MethodUtilGenericsContext.java | 85 +++ ...thodBindingMethodParameterBuilderTest.java | 46 +- ...perationParamsInnerClassesAndMethods.java} | 226 ++++++- .../rest/server/method/MethodUtilTest.java | 608 +++++++++++++++--- .../method/OperationMethodBindingTest.java | 18 +- 6 files changed, 869 insertions(+), 178 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java rename hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/{EmbeddedParamsInnerClassesAndMethods.java => MethodAndOperationParamsInnerClassesAndMethods.java} (68%) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index a88edf3c011d..0c5c664d2d49 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -102,7 +102,6 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] } } - // LUKETODO: extract annotations method and make sure it works with embedded params public static List getResourceParameters( final FhirContext theContext, final Method theMethod, Object theProvider) { // We mutate this variable so distinguish this from the argument to getResourceParameters @@ -124,7 +123,7 @@ public static List getResourceParameters( // TagList is handled directly within the method bindings param = new NullParameter(); } else { - final GenericsContext genericsContext = + final MethodUtilGenericsContext genericsContext = getGenericsContext(theContext, theMethod, parameterTypes, paramIndex); parameterType = genericsContext.getParameterType(); @@ -565,7 +564,7 @@ private static ParamInitializationContext createOperationParamContext( param, parameterTypeInner, theOuterCollectionType, theInnerCollectionType); } - private static GenericsContext getGenericsContext( + private static MethodUtilGenericsContext getGenericsContext( FhirContext theContext, Method theMethod, Class[] theParameterTypes, int theParamIndex) { Class declaredParameterType = theParameterTypes[theParamIndex]; @@ -628,67 +627,12 @@ private static GenericsContext getGenericsContext( } } - return new GenericsContext(parameterType, declaredParameterType, outerCollectionType, innerCollectionType); + return new MethodUtilGenericsContext( + parameterType, declaredParameterType, outerCollectionType, innerCollectionType); } @SuppressWarnings("unchecked") private static T unsafeCast(Object theObject) { return (T) theObject; } - - // LUKETODO: top level? - private static class GenericsContext { - private final Class parameterType; - private final Class declaredParameterType; - private final Class> outerCollectionType; - private final Class> innerCollectionType; - - public GenericsContext( - Class theParameterType, - Class theDeclaredParameterType, - Class> theOuterCollectionType, - Class> theInnerCollectionType) { - parameterType = theParameterType; - declaredParameterType = theDeclaredParameterType; - outerCollectionType = theOuterCollectionType; - innerCollectionType = theInnerCollectionType; - } - - public Class getParameterType() { - return parameterType; - } - - public Class getDeclaredParameterType() { - return declaredParameterType; - } - - public Class> getOuterCollectionType() { - return outerCollectionType; - } - - public Class> getInnerCollectionType() { - return innerCollectionType; - } - } - - // LUKETODO: refactor to use only one of the Context classes - private static class ParameterContext { - private final Class myParameterType; - - @Nullable - private final IParameter myParameter; - - public ParameterContext(Class theParameterType, @Nullable IParameter theParameter) { - myParameter = theParameter; - myParameterType = theParameterType; - } - - public IParameter getParam() { - return myParameter; - } - - public Class getParameterType() { - return myParameterType; - } - } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java new file mode 100644 index 000000000000..956b99649ca6 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java @@ -0,0 +1,85 @@ +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.server.method; + +import java.util.Collection; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Simple POJO to capture details of MethodUtil generic type information for method params, if there is any. + */ +class MethodUtilGenericsContext { + private final Class parameterType; + private final Class declaredParameterType; + private final Class> outerCollectionType; + private final Class> innerCollectionType; + + public MethodUtilGenericsContext( + Class theParameterType, + Class theDeclaredParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType) { + parameterType = theParameterType; + declaredParameterType = theDeclaredParameterType; + outerCollectionType = theOuterCollectionType; + innerCollectionType = theInnerCollectionType; + } + + public Class getParameterType() { + return parameterType; + } + + public Class getDeclaredParameterType() { + return declaredParameterType; + } + + public Class> getOuterCollectionType() { + return outerCollectionType; + } + + public Class> getInnerCollectionType() { + return innerCollectionType; + } + + @Override + public boolean equals(Object theO) { + if (theO == null || getClass() != theO.getClass()) { + return false; + } + MethodUtilGenericsContext that = (MethodUtilGenericsContext) theO; + return Objects.equals(parameterType, that.parameterType) && Objects.equals(declaredParameterType, that.declaredParameterType) && Objects.equals(outerCollectionType, that.outerCollectionType) && Objects.equals(innerCollectionType, that.innerCollectionType); + } + + @Override + public int hashCode() { + return Objects.hash(parameterType, declaredParameterType, outerCollectionType, innerCollectionType); + } + + @Override + public String toString() { + return new StringJoiner(", ", MethodUtilGenericsContext.class.getSimpleName() + "[", "]") + .add("parameterType=" + parameterType) + .add("declaredParameterType=" + declaredParameterType) + .add("outerCollectionType=" + outerCollectionType) + .add("innerCollectionType=" + innerCollectionType) + .toString(); + } +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index faac4a76678b..9d6277fee2fe 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -4,7 +4,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParams; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; @@ -18,16 +18,16 @@ import java.time.ZoneOffset; import java.util.List; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithoutAnnotations; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithTypeConversion; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithoutAnnotations; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParamsWithIdParam; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -42,7 +42,7 @@ class BaseMethodBindingMethodParameterBuilderTest { private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); - private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); + private final MethodAndOperationParamsInnerClassesAndMethods myMethodAndOperationParamsInnerClassesAndMethods = new MethodAndOperationParamsInnerClassesAndMethods(); // LUKETODO: wrong params // LUKETODO: wrong param order @@ -51,7 +51,7 @@ class BaseMethodBindingMethodParameterBuilderTest { @Test void happyPathOperationParamsEmptyParams() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(MethodAndOperationParamsInnerClassesAndMethods.SUPER_SIMPLE); final Object[] inputParams = new Object[]{}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); @@ -61,7 +61,7 @@ void happyPathOperationParamsEmptyParams() { @Test void happyPathOperationParamsNonEmptyParams() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", List.of("param2")}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); @@ -71,7 +71,7 @@ void happyPathOperationParamsNonEmptyParams() { @Test void happyPathOperationEmbeddedTypesNoRequestDetails() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{"param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param2"))}; @@ -82,7 +82,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetails() { @Test void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); final Object[] inputParams = new Object[]{null, null}; final Object[] expectedOutputParams = new Object[]{new SampleParams(null, null)}; @@ -93,7 +93,7 @@ void happyPathOperationEmbeddedTypesNoRequestDetailsNullArguments() { @Test void happyPathOperationEmbeddedTypesRequestDetailsFirst() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, "param1", List.of("param2")}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParams("param1", List.of("param2"))}; @@ -104,7 +104,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsFirst() { @Test void happyPathOperationEmbeddedTypesRequestDetailsLast() { - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{"param1", List.of("param3"), REQUEST_DETAILS}; final Object[] expectedOutputParams = new Object[]{new SampleParams("param1", List.of("param3")), REQUEST_DETAILS}; @@ -117,7 +117,7 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { @Disabled void happyPathOperationEmbeddedTypesWithIdType() { final IdType id = new IdType(); - final Method sampleMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; @@ -135,7 +135,7 @@ void buildMethodParams_withNullMethod_shouldThrowInternalErrorException() { @Test void buildMethodParams_withNullParams_shouldThrowInternalErrorException() throws NoSuchMethodException { - final Method sampleMethod = EmbeddedParamsInnerClassesAndMethods.class.getDeclaredMethod(EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE); + final Method sampleMethod = MethodAndOperationParamsInnerClassesAndMethods.class.getDeclaredMethod(MethodAndOperationParamsInnerClassesAndMethods.SUPER_SIMPLE); assertThrows(InternalErrorException.class, () -> { buildMethodParams(sampleMethod, REQUEST_DETAILS, null); @@ -149,7 +149,7 @@ void buildMethodParams_withNullMethodAndParams_shouldThrowInternalErrorException @Test void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException() { - final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, + final Method method = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS, RequestDetails.class, SampleParams.class, RequestDetails.class); final Object[] inputParams = new Object[]{REQUEST_DETAILS, new IdDt(), "param1", List.of("param2", REQUEST_DETAILS)}; assertThrows(InternalErrorException.class, () -> { @@ -161,7 +161,7 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( @Test @Disabled void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { - final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); + final Method method = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); final Object[] inputParams = new Object[]{new IdDt(), "param1", 2, List.of("param3")}; @@ -172,7 +172,7 @@ void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternal @Test void paramsConversionZonedDateTime() { - final Method method = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); + final Method method = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); final Object[] inputParams = new Object[]{"2024-01-01", "2025-01-01"}; final Object[] expectedOutputParams = new Object[]{ diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java similarity index 68% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java index c3be528d0d2c..bef6da4216d4 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java @@ -1,17 +1,43 @@ package ca.uhn.fhir.rest.server.method; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.At; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Count; +import ca.uhn.fhir.rest.annotation.Elements; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; -import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.ServerBase; +import ca.uhn.fhir.rest.annotation.Since; +import ca.uhn.fhir.rest.annotation.Sort; +import ca.uhn.fhir.rest.annotation.TransactionParam; +import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.IResourceProvider; import jakarta.annotation.Nullable; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; @@ -23,6 +49,7 @@ import java.lang.reflect.Method; import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.StringJoiner; @@ -32,11 +59,17 @@ // This class lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes -class EmbeddedParamsInnerClassesAndMethods { - +class MethodAndOperationParamsInnerClassesAndMethods { + + static final String METHOD_WITH_DESCRIPTION = "methodWithDescription"; + static final String METHOD_WITH_INVALID_GENERIC_TYPE = "methodWithInvalidGenericType"; + static final String METHOD_WITH_UNKNOWN_TYPE_NAME = "methodWithUnknownTypeName"; + static final String METHOD_WITH_NON_ASSIGNABLE_TYPE_NAME = "methodWithNonAssignableTypeName"; + static final String METHOD_WITH_NO_ANNOTATIONS = "methodWithNoAnnotations"; + static final String METHOD_WITH_INVALID_ANNOTATION = "methodWithInvalidAnnotation"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS = "sampleMethodEmbeddedTypeMultipleRequestDetails"; - static final String SUPER_SIMPLE = "superSimple"; static final String SIMPLE_OPERATION = "simpleOperation"; + static final String SUPER_SIMPLE = "superSimple"; static final String INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION = "invalidMethodOperationParamsNoOperationInvalid"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE = "sampleMethodEmbeddedTypeRequestDetailsFirstWithIdType"; static final String SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST = "sampleMethodEmbeddedTypeRequestDetailsLast"; @@ -99,26 +132,28 @@ public void methodWithInvalidAnnotation(String param) { } void superSimple() { + // No Implementation } @Operation(name = "") void invalidOperation() { - + // No Implementation } @Operation(name = "$simpleOperation") void simpleOperation() { - + // No Implementation } @Operation(name = "$withEmbeddedParams") void withEmbeddedParams() { - + // No Implementation } void invalidMethodOperationParamsNoOperationInvalid( @OperationParam(name = "param1") String theParam1) { + // No Implementation } @Operation(name="sampleMethodOperationParams", type = Measure.class) @@ -469,4 +504,181 @@ Parameters opInstanceOrType( return new Parameters(); } } + + @Description( + shortDefinition="network identifier", + example="An identifier for the network access point of the user device for the audit event" + ) + public void methodWithDescription() { + // No Implementation + } + + // Basic Search Parameters + public void methodWithRequiredParam(@RequiredParam(name = "requiredParam") String param) { + // No Implementation + } + public void methodWithOptionalParam(@OptionalParam(name = "optionalParam") String param) { + // No Implementation + } + public void invalidOptionalParamInteger(@OptionalParam(name = "optionalParam") Integer param) { + // No Implementation + } + + // ResourceParam + public void methodWithResourceParam(@ResourceParam IBaseResource resource) { + // No Implementation + } + public void methodWithResourceParamString(@ResourceParam String resourceString) { + // No Implementation + } + public void methodWithResourceParamByteArray(@ResourceParam byte[] resourceBytes) { + // No Implementation + } + + // Servlet/Request parameters + public void methodWithServletRequest(ServletRequest request) { + // No Implementation + } + public void methodWithServletResponse(ServletResponse response) { + // No Implementation + } + public void methodWithRequestDetails(RequestDetails details) { + // No Implementation + } + public void methodWithInterceptorBroadcaster(IInterceptorBroadcaster broadcaster) { + // No Implementation + } + + // ID Param -> NullParameter + public void methodWithIdParam(@IdParam String theId) { + // No Implementation + } + + // Server Base -> ServerBaseParamBinder + public void methodWithServerBase(@ServerBase String base) { + // No Implementation + } + + // Elements -> ElementsParameter + public void methodWithElements(@Elements String elements) { + // No Implementation + } + + // Since -> SinceParameter + public void methodWithSince(@Since Date sinceDate) { + // No Implementation + } + + // At -> AtParameter + public void methodWithAt(@At Date atDate) { + // No Implementation + } + + // Count -> CountParameter + public void methodWithCount(@Count Integer count) { + // No Implementation + } + + // Offset -> OffsetParameter + public void methodWithOffset(@Offset Integer offset) { + // No Implementation + } + + // SummaryEnum -> SummaryEnumParameter + public void methodWithSummaryEnum(SummaryEnum summary) { + // No Implementation + } + + // PatchTypeEnum -> PatchTypeParameter + public void methodWithPatchType(PatchTypeEnum patchType) { + // No Implementation + } + + // SearchContainedModeEnum -> SearchContainedModeParameter + public void methodWithSearchContainedMode(SearchContainedModeEnum containedMode) { + // No Implementation + } + + // SearchTotalModeEnum -> SearchTotalModeParameter + public void methodWithSearchTotalMode(ca.uhn.fhir.rest.api.SearchTotalModeEnum totalMode) { + // No Implementation + } + + // GraphQL Query Url -> GraphQLQueryUrlParameter + public void methodWithGraphQLQueryUrl(@GraphQLQueryUrl String queryUrl) { + // No Implementation + } + + // GraphQL Query Body -> GraphQLQueryBodyParameter + public void methodWithGraphQLQueryBody(@GraphQLQueryBody String queryBody) { + // No Implementation + } + + // Sort -> SortParameter + public void invalidMethodWithSort(@Sort String sort) { + // no implementation + } + + public void methodWithSort(@Sort SortSpec sort) { + // no implementation + } + + // TransactionParam -> TransactionParameter + public void methodWithTransactionParam(@TransactionParam IBaseResource bundle) { + // no implementation + } + + // ConditionalUrlParam -> ConditionalParamBinder + public void methodWithConditionalUrlParam(@ConditionalUrlParam String conditionalUrl) { + // no implementation + } + + // OperationParam (requires @Operation) + @Operation(name = "opTest", idempotent = true) + public void methodWithOperationParam(@OperationParam(name = "opParam") String opParam) { + // no implementation + } + + // OperationParam with typeName + @Operation(name = "opTest2", idempotent = true) + public void methodWithOperationParamAndTypeName(@OperationParam(name = "opParamTyped", typeName = "string") StringType typedParam) { + // no implementation + } + + @Operation(name = "opTest2", idempotent = true) + public void invalidMethodWithOperationParamAndTypeName(@OperationParam(name = "opParamTyped", typeName = "string") String typedParam) { + // no implementation + } + + // Validate -> Validate.Mode and Validate.Profile + public void methodWithValidateAnnotations(@Validate.Mode ValidationModeEnum mode, + @Validate.Profile String profile) { + // no implementation + } + + // Unknown param -> no recognized annotation, should fail + public void methodWithUnknownParam(Double someParam) { + // no implementation + } + + // TagList -> should become NullParameter + public void methodWithTagList(TagList tagList) { + // no implementation + } + + // Force a Collection of Collections => error + public void methodWithCollectionOfCollections(List> doubleCollection) { + // no implementation + } + + // Force a param IPrimitiveType => triggers special code path + public void methodWithIPrimitiveTypeDate( + @RequiredParam(name = "primitiveTypeDateParam") IPrimitiveType listParam) { + // no implementation + } + + public void invalidMethodWithIPrimitiveTypeDate( + @RequiredParam(name = "primitiveTypeDateParam") List> listParam) { + // no implementation + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index c7571e91acf0..752ca7266f3a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -2,53 +2,85 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithIdParamAndTypeConversion; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.ParamsWithTypeConversion; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParams; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithIdParamAndTypeConversion; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithTypeConversion; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParams; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParamsWithIdParam; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.List; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SUPER_SIMPLE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_DESCRIPTION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_INVALID_ANNOTATION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_INVALID_GENERIC_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_NON_ASSIGNABLE_TYPE_NAME; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_NO_ANNOTATIONS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.METHOD_WITH_UNKNOWN_TYPE_NAME; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.OP_INSTANCE_OR_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SIMPLE_METHOD_WITH_PARAMS_CONVERSION; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; // This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency class MethodUtilTest { - private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); - private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); - private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); + private final MethodAndOperationParamsInnerClassesAndMethods myMethodAndOperationParamsInnerClassesAndMethods = new MethodAndOperationParamsInnerClassesAndMethods(); - private final Object myProvider = new Object(); + private Object myProvider = null; @Test void simpleMethodNoParams() { final List resourceParameters = getMethodAndExecute(SUPER_SIMPLE); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isEmpty(); + assertThat(resourceParameters).isNotNull().isEmpty(); } @Test @@ -59,12 +91,45 @@ void invalid_methodWithOperationParamsNoOperation() { .isInstanceOf(ConfigurationException.class); } + @Test + void invalidMethodWithNoAnnotations() { + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_NO_ANNOTATIONS, String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); + } + + @Test + void invalidMethodWithInvalidGenericType() { + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_INVALID_GENERIC_TYPE, List.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("is of an invalid generic type"); + } + + @Test + void invalidMethodWithUnknownTypeName() { + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_UNKNOWN_TYPE_NAME, String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); + } + + @Test + void invalidMethodWithNonAssignableTypeName() { + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_NON_ASSIGNABLE_TYPE_NAME, String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining(" has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); + } + + @Test + void invalidMethodWithInvalidAnnotation() { + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_INVALID_ANNOTATION, String.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); + } + @Test void sampleMethodOperationParams() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( @@ -79,13 +144,54 @@ void sampleMethodOperationParams() { "Expected parameters do not match actual parameters"); } + @Test + void expand() { + final List resourceParameters = getMethodAndExecute(EXPAND, HttpServletRequest.class, IIdType.class, IBaseResource.class, RequestDetails.class); + + assertThat(resourceParameters).isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(ServletRequestParameter.class, NullParameter.class, OperationParameter.class, RequestDetailsParameter.class); + + final List expectedParameters = List.of( + new ServletRequestParameterToAssert(), + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "valueSet", "$"+EXPAND, null, String.class, "Resource", Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new RequestDetailsParameterToAssert() + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); + } + + @Test + void opInstanceOrType() { + myProvider = new MethodAndOperationParamsInnerClassesAndMethods.PatientProvider(); + final List resourceParameters = getMethodAndExecute(OP_INSTANCE_OR_TYPE, IdType.class, StringType.class, Patient.class); + + assertThat(resourceParameters).isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "PARAM1", "$OP_INSTANCE_OR_TYPE", null, String.class, "string", Void.class, OperationParameterRangeType.NOT_APPLICABLE), + new OperationParameterToAssert(ourFhirContext, "PARAM2", "$OP_INSTANCE_OR_TYPE", null, String.class, "Patient", Void.class, OperationParameterRangeType.NOT_APPLICABLE) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); + } + @Test void sampleMethodOperationParamsWithFhirTypes() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_OPERATION_PARAMS, IIdType.class, String.class, List.class, BooleanType.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), @@ -103,9 +209,10 @@ void sampleMethodOperationParamsWithFhirTypes() { void sampleMethodEmbeddedParams() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, SampleParams.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), @@ -117,13 +224,376 @@ void sampleMethodEmbeddedParams() { "Expected parameters do not match actual parameters"); } + // ------------------------------------------------------------------------- + // 1. Testing MethodUtil.extractDescription() + // ------------------------------------------------------------------------- + @Test + void testExtractDescription_withDescriptionAnnotation() { + final Method method = getMethod(METHOD_WITH_DESCRIPTION); + final Annotation[] annotations = method.getAnnotations(); + + assertThat(annotations).hasSize(1); + + final Annotation annotation = annotations[0]; + + assertThat(annotation).isInstanceOf(Description.class); + + final Description descriptionAnnotation = (Description) annotation; + + assertThat(descriptionAnnotation.shortDefinition()).isEqualTo("network identifier"); + assertThat(descriptionAnnotation.example()).isEqualTo(new String[]{"An identifier for the network access point of the user device for the audit event"}); + } + + @Test + void testExtractDescription_noDescriptionAnnotation() { + SearchParameter parameter = new SearchParameter(); + Annotation[] annotations = new Annotation[]{}; + + // Should simply do nothing (no exception thrown, no change) + MethodUtil.extractDescription(parameter, annotations); + + assertNull(parameter.getDescription()); + } + + // ------------------------------------------------------------------------- + // 2. Testing MethodUtil.getResourceParameters() - Annotation Scenarios + // ------------------------------------------------------------------------- + + @Test + void testRequiredParam() { + final List params = getMethodAndExecute("methodWithRequiredParam", String.class); + + assertEquals(1, params.size()); + assertInstanceOf(SearchParameter.class, params.get(0)); + final SearchParameter sp = (SearchParameter) params.get(0); + assertEquals("requiredParam", sp.getName()); + assertTrue(sp.isRequired()); + } + + @Test + void testOptionalParam() { + final List params = getMethodAndExecute("methodWithOptionalParam", String.class); + + assertEquals(1, params.size()); + assertInstanceOf(SearchParameter.class, params.get(0)); + final SearchParameter sp = (SearchParameter) params.get(0); + assertEquals("optionalParam", sp.getName()); + assertFalse(sp.isRequired()); + } + + @Test + void invalidOptionalParam() { + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("invalidOptionalParamInteger", Integer.class)); + } + + @Test + void testResourceParamAsIBaseResource() { + final List params = getMethodAndExecute("methodWithResourceParam", IBaseResource.class); + + assertEquals(1, params.size()); + assertInstanceOf(ResourceParameter.class, params.get(0)); + ResourceParameter rp = (ResourceParameter) params.get(0); + assertNotNull(rp); + } + + @Test + void testResourceParamAsString() { + final List params = getMethodAndExecute("methodWithResourceParamString", String.class); + + assertEquals(1, params.size()); + assertInstanceOf(ResourceParameter.class, params.get(0)); + } + + @Test + void testResourceParamAsByteArray() { + final List params = getMethodAndExecute("methodWithResourceParamByteArray", byte[].class); + + assertEquals(1, params.size()); + assertInstanceOf(ResourceParameter.class, params.get(0)); + } + + @Test + void testServletRequestParameter() { + final List params = getMethodAndExecute("methodWithServletRequest", ServletRequest.class); + + assertEquals(1, params.size()); + assertInstanceOf(ServletRequestParameter.class, params.get(0)); + } + + @Test + void testServletResponseParameter() { + final List params = getMethodAndExecute("methodWithServletResponse", ServletResponse.class); + + assertEquals(1, params.size()); + assertInstanceOf(ServletResponseParameter.class, params.get(0)); + } + + @Test + void testRequestDetailsParameter() { + final List params = getMethodAndExecute("methodWithRequestDetails", RequestDetails.class); + + assertEquals(1, params.size()); + assertInstanceOf(RequestDetailsParameter.class, params.get(0)); + } + + @Test + void testInterceptorBroadcasterParameter() { + final List params = getMethodAndExecute("methodWithInterceptorBroadcaster", IInterceptorBroadcaster.class); + + assertEquals(1, params.size()); + assertInstanceOf(InterceptorBroadcasterParameter.class, params.get(0)); + } + + @Test + void testIdParamParameter() { + final List params = getMethodAndExecute("methodWithIdParam", String.class); + + // Should produce a NullParameter based on "IdParam" + assertEquals(1, params.size()); + assertInstanceOf(NullParameter.class, params.get(0)); + } + + @Test + void testServerBaseParameter() { + final List params = getMethodAndExecute("methodWithServerBase", String.class); + + // Expect ServerBaseParamBinder + assertEquals(1, params.size()); + assertInstanceOf(ServerBaseParamBinder.class, params.get(0)); + } + + @Test + void testElementsParameter() { + final List params = getMethodAndExecute("methodWithElements", String.class); + + // Expect ElementsParameter + assertEquals(1, params.size()); + assertInstanceOf(ElementsParameter.class, params.get(0)); + } + + @Test + void testSinceParameter() { + final List params = getMethodAndExecute("methodWithSince", Date.class); + + // Expect SinceParameter + assertEquals(1, params.size()); + assertInstanceOf(SinceParameter.class, params.get(0)); + } + + @Test + void testAtParameter() { + final List params = getMethodAndExecute("methodWithAt", Date.class); + + // Expect AtParameter + assertEquals(1, params.size()); + assertInstanceOf(AtParameter.class, params.get(0)); + } + + @Test + void testCountParameter() { + final List params = getMethodAndExecute("methodWithCount", Integer.class); + + // Expect CountParameter + assertEquals(1, params.size()); + assertInstanceOf(CountParameter.class, params.get(0)); + } + + @Test + void testOffsetParameter() { + final List params = getMethodAndExecute("methodWithOffset", Integer.class); + + // Expect OffsetParameter + assertEquals(1, params.size()); + assertInstanceOf(OffsetParameter.class, params.get(0)); + } + + @Test + void testSummaryEnumParameter() { + final List params = getMethodAndExecute("methodWithSummaryEnum", SummaryEnum.class); + + // Expect SummaryEnumParameter + assertEquals(1, params.size()); + assertInstanceOf(SummaryEnumParameter.class, params.get(0)); + } + + @Test + void testPatchTypeParameter() { + final List params = getMethodAndExecute("methodWithPatchType", PatchTypeEnum.class); + + // Expect PatchTypeParameter + assertEquals(1, params.size()); + assertInstanceOf(PatchTypeParameter.class, params.get(0)); + } + + @Test + void testSearchContainedModeParameter() { + final List params = getMethodAndExecute("methodWithSearchContainedMode", SearchContainedModeEnum.class); + + // Expect SearchContainedModeParameter + assertEquals(1, params.size()); + assertInstanceOf(SearchContainedModeParameter.class, params.get(0)); + } + + @Test + void testSearchTotalModeParameter() { + final List params = getMethodAndExecute("methodWithSearchTotalMode", SearchTotalModeEnum.class); + + // Expect SearchTotalModeParameter + assertEquals(1, params.size()); + assertInstanceOf(SearchTotalModeParameter.class, params.get(0)); + } + + @Test + void testGraphQLQueryUrlParameter() { + final List params = getMethodAndExecute("methodWithGraphQLQueryUrl", String.class); + + // Expect GraphQLQueryUrlParameter + assertEquals(1, params.size()); + assertInstanceOf(GraphQLQueryUrlParameter.class, params.get(0)); + } + + @Test + void testGraphQLQueryBodyParameter() { + final List params = getMethodAndExecute("methodWithGraphQLQueryBody", String.class); + + // Expect GraphQLQueryBodyParameter + assertEquals(1, params.size()); + assertInstanceOf(GraphQLQueryBodyParameter.class, params.get(0)); + } + + @Test + void testSortParameter() { + final List params = getMethodAndExecute("methodWithSort", SortSpec.class); + + // Expect SortParameter + assertEquals(1, params.size()); + assertInstanceOf(SortParameter.class, params.get(0)); + } + + @Test + void testInvalidSortSpec() { + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("invalidMethodWithSort", String.class)); + } + + + @Test + void testTransactionParameter() { + final List params = getMethodAndExecute("methodWithTransactionParam", IBaseResource.class); + + // Expect TransactionParameter + assertEquals(1, params.size()); + assertInstanceOf(TransactionParameter.class, params.get(0)); + } + + @Test + void testConditionalParamBinder() { + final List params = getMethodAndExecute("methodWithConditionalUrlParam", String.class); + + // Expect ConditionalParamBinder + assertEquals(1, params.size()); + assertInstanceOf(ConditionalParamBinder.class, params.get(0)); + } + + // ------------------------------------------------------------------------- + // 3. Testing MethodUtil.getResourceParameters() - OperationParam + // ------------------------------------------------------------------------- + @Test + void testOperationParam() { + // This method is annotated @Operation, so any @OperationParam is recognized + final List params = getMethodAndExecute("methodWithOperationParam", String.class); + + assertEquals(1, params.size()); + assertInstanceOf(OperationParameter.class, params.get(0)); + OperationParameter opParam = (OperationParameter) params.get(0); + assertEquals("opParam", opParam.getName()); + } + + @Test + void testOperationParamWithTypeName() { + final List params = getMethodAndExecute("methodWithOperationParamAndTypeName", StringType.class); + + assertEquals(1, params.size()); + assertInstanceOf(OperationParameter.class, params.get(0)); + OperationParameter opParam = (OperationParameter) params.get(0); + assertEquals("opParamTyped", opParam.getName()); + } + + @Test + void invalidOperationParamWithTypeName() { + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("invalidMethodWithOperationParamAndTypeName", String.class)); + } + + @Test + void testValidateModeAndProfile() { + final List params = getMethodAndExecute("methodWithValidateAnnotations", ValidationModeEnum.class, String.class); + + assertEquals(2, params.size()); + + // ValidateMode => OperationParameter with param name "mode" + assertInstanceOf(OperationParameter.class, params.get(0)); + OperationParameter modeParam = (OperationParameter) params.get(0); + assertEquals(Constants.EXTOP_VALIDATE_MODE, modeParam.getName()); + + // ValidateProfile => OperationParameter with param name "profile" + assertInstanceOf(OperationParameter.class, params.get(1)); + OperationParameter profileParam = (OperationParameter) params.get(1); + assertEquals(Constants.EXTOP_VALIDATE_PROFILE, profileParam.getName()); + } + + // ------------------------------------------------------------------------- + // 4. Testing MethodUtil.getResourceParameters() - Edge/Exception Cases + // ------------------------------------------------------------------------- + @Test + void testUnknownParameter() { + // methodWithUnknownParam has no recognized annotations => triggers error + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("methodWithUnknownParam", Double.class)); + } + + @Test + void testTagListParameter() { + // Should produce a NullParameter, as TagList param is handled separately + final List params = getMethodAndExecute("methodWithTagList", TagList.class); + + assertEquals(1, params.size()); + assertInstanceOf(NullParameter.class, params.get(0)); + } + + @Test + void testCollectionOfCollectionParameter() { + // This will trigger an exception due to multiple levels of collection generics + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("methodWithCollectionOfCollections", List.class)); + } + + @Test + void methodWithIPrimitiveTypeDate() { + final List params = getMethodAndExecute("methodWithIPrimitiveTypeDate", IPrimitiveType.class); + + // Expect a SearchParameter + assertEquals(1, params.size()); + assertInstanceOf(SearchParameter.class, params.get(0)); + SearchParameter sp = (SearchParameter) params.get(0); + assertEquals("primitiveTypeDateParam", sp.getName()); + } + + @Test + void invalidMethodWithIPrimitiveTypeDate() { + assertThrows(ConfigurationException.class, () -> + getMethodAndExecute("invalidMethodWithIPrimitiveTypeDate", List.class)); + } + @Test void sampleMethodEmbeddedParamsRequestDetailsFirst() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST, RequestDetails.class, SampleParams.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new RequestDetailsParameterToAssert(), @@ -140,9 +610,10 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { void sampleMethodEmbeddedParamsRequestDetailsLast() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST, SampleParams.class, RequestDetails.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class, RequestDetailsParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class, RequestDetailsParameter.class); final List expectedParameters = List.of( new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_LAST,null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), @@ -159,9 +630,10 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { void sampleMethodEmbeddedParamsWithFhirTypes() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, SampleParamsWithIdParam.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), @@ -179,9 +651,10 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { void paramsConversionZonedDateTime() { final List resourceParameters = getMethodAndExecute(SIMPLE_METHOD_WITH_PARAMS_CONVERSION, ParamsWithTypeConversion.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new OperationParameterToAssert(ourFhirContext, "periodStart", SIMPLE_METHOD_WITH_PARAMS_CONVERSION,null, ZonedDateTime.class, null, String.class, OperationParameterRangeType.START), @@ -197,9 +670,10 @@ void paramsConversionZonedDateTime() { void paramsConversionIdTypeZonedDateTime() { final List resourceParameters = getMethodAndExecute(SAMPLE_METHOD_EMBEDDED_TYPE_ID_TYPE_AND_TYPE_CONVERSION, ParamsWithIdParamAndTypeConversion.class); - assertThat(resourceParameters).isNotNull(); - assertThat(resourceParameters).isNotEmpty(); - assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + assertThat(resourceParameters) + .isNotNull() + .isNotEmpty() + .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), @@ -212,48 +686,17 @@ void paramsConversionIdTypeZonedDateTime() { "Expected parameters do not match actual parameters"); } - @Test - void invalidMethodWithNoAnnotations() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithNoAnnotations", String.class)) - .isInstanceOf(ConfigurationException.class) - .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); - } - - @Test - void invalidMethodWithInvalidGenericType() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithInvalidGenericType", List.class)) - .isInstanceOf(ConfigurationException.class) - .hasMessageContaining("is of an invalid generic type"); - } - - @Test - void invalidMethodWithUnknownTypeName() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithUnknownTypeName", String.class)) - .isInstanceOf(ConfigurationException.class) - .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); - } - - @Test - void invalidMethodWithNonAssignableTypeName() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithNonAssignableTypeName", String.class)) - .isInstanceOf(ConfigurationException.class) - .hasMessageContaining(" has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); - } - - @Test - void invalidMethodWithInvalidAnnotation() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithInvalidAnnotation", String.class)) - .isInstanceOf(ConfigurationException.class) - .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); - } - private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( ourFhirContext, - myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), + getMethod(theMethodName, theParamClasses), myProvider); } + private Method getMethod(String theTheMethodName, Class... theTheParamClasses) { + return myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, theTheMethodName, theTheParamClasses); + } + private boolean assertParametersEqual(List theExpectedParameters, List theActualParameters) { if (theActualParameters.size() != theExpectedParameters.size()) { fail("Expected parameters size does not match actual parameters size"); @@ -281,6 +724,10 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I return true; } + if (theExpectedParameter instanceof ServletRequestParameterToAssert && theActualParameter instanceof ServletRequestParameter) { + return true; + } + if (theExpectedParameter instanceof OperationParameterToAssert expectedOperationParameter && theActualParameter instanceof OperationParameter actualOperationParameter) { assertThat(actualOperationParameter.getOperationName()).isEqualTo(expectedOperationParameter.myOperationName()); assertThat(actualOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationParameter.myContext().getVersion().getVersion()); @@ -301,6 +748,9 @@ private interface IParameterToAssert {} private record NullParameterToAssert() implements IParameterToAssert { } + private record ServletRequestParameterToAssert() implements IParameterToAssert { + } + private record RequestDetailsParameterToAssert() implements IParameterToAssert { } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 448f2f0b693a..83447b0d1e15 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.rest.server.method; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.EXPAND; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.OP_INSTANCE_OR_TYPE; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; -import static ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.OP_INSTANCE_OR_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_OPERATION_PARAMS; import static org.junit.jupiter.api.Assertions.*; import ca.uhn.fhir.context.ConfigurationException; @@ -13,8 +13,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.PatientProvider; -import ca.uhn.fhir.rest.server.method.EmbeddedParamsInnerClassesAndMethods.SampleParamsWithIdParam; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.PatientProvider; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParamsWithIdParam; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -34,7 +34,7 @@ class OperationMethodBindingTest { private static final FhirContext ourFhirContext = FhirContext.forR4Cached(); - private final EmbeddedParamsInnerClassesAndMethods myEmbeddedParamsInnerClassesAndMethods = new EmbeddedParamsInnerClassesAndMethods(); + private final MethodAndOperationParamsInnerClassesAndMethods myMethodAndOperationParamsInnerClassesAndMethods = new MethodAndOperationParamsInnerClassesAndMethods(); private Method myMethod; private Operation myOperation; @@ -83,7 +83,7 @@ void incomingServerRequestMatchesMethod_withMatchingOperation_shouldReturnExact( @Test void invokeServer_withUnsupportedRequestType_shouldThrowMethodNotAllowedException() { - init(EmbeddedParamsInnerClassesAndMethods.SIMPLE_OPERATION); + init(MethodAndOperationParamsInnerClassesAndMethods.SIMPLE_OPERATION); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.PUT); @@ -162,7 +162,7 @@ void methodWithIdParamButNoIIdType() { } private void init(String theMethodName, Class... theParamClasses) { - myMethod = myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses); + myMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses); myOperation = myMethod.getAnnotation(Operation.class); } } From da05e9dec2e99bb0e66d8da1f6a9308e48a90a52 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 29 Jan 2025 09:23:22 -0500 Subject: [PATCH 70/75] Spotless. Convert types in parmas constructors. Fix Builder constructors. --- .../server/method/EmbeddedOperationUtils.java | 13 +++++--- .../method/MethodUtilGenericsContext.java | 15 +++++---- .../r4/measure/CareGapsOperationProvider.java | 8 +---- .../fhir/cr/r4/measure/CareGapsParams.java | 33 ++++++++++++------- .../measure/EvaluateMeasureSingleParams.java | 28 +++++++++------- 5 files changed, 56 insertions(+), 41 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index e9d19a088f3b..370eaa444af5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -26,6 +26,8 @@ import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import jakarta.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -46,6 +48,7 @@ * Common operations for any functionality that work with {@link EmbeddedOperationParams} */ public class EmbeddedOperationUtils { + private static final Logger ourLog = LoggerFactory.getLogger(EmbeddedOperationUtils.class); private EmbeddedOperationUtils() {} @@ -230,10 +233,12 @@ private static void validateConstructorArgs(Constructor theConstructor, Field } if (constructorParameterTypeAtIndex != fieldTypeAtIndex) { - final String error = String.format( - "%sInvalid operation embedded parameters. Constructor parameter type does not match field type: %s", - Msg.code(87421741), theConstructor.getDeclaringClass()); - throw new ConfigurationException(error); + // LUKETODO: debug + ourLog.info( + "constructor: {} parameter type: {} is transformed to field type: {}", + theConstructor.getDeclaringClass(), + constructorParameterTypeAtIndex, + fieldTypeAtIndex); } if (Collection.class.isAssignableFrom(constructorParameterTypeAtIndex) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java index 956b99649ca6..f99bae5f0800 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtilGenericsContext.java @@ -65,7 +65,10 @@ public boolean equals(Object theO) { return false; } MethodUtilGenericsContext that = (MethodUtilGenericsContext) theO; - return Objects.equals(parameterType, that.parameterType) && Objects.equals(declaredParameterType, that.declaredParameterType) && Objects.equals(outerCollectionType, that.outerCollectionType) && Objects.equals(innerCollectionType, that.innerCollectionType); + return Objects.equals(parameterType, that.parameterType) + && Objects.equals(declaredParameterType, that.declaredParameterType) + && Objects.equals(outerCollectionType, that.outerCollectionType) + && Objects.equals(innerCollectionType, that.innerCollectionType); } @Override @@ -76,10 +79,10 @@ public int hashCode() { @Override public String toString() { return new StringJoiner(", ", MethodUtilGenericsContext.class.getSimpleName() + "[", "]") - .add("parameterType=" + parameterType) - .add("declaredParameterType=" + declaredParameterType) - .add("outerCollectionType=" + outerCollectionType) - .add("innerCollectionType=" + innerCollectionType) - .toString(); + .add("parameterType=" + parameterType) + .add("declaredParameterType=" + declaredParameterType) + .add("outerCollectionType=" + outerCollectionType) + .add("innerCollectionType=" + innerCollectionType) + .toString(); } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 8ada0d58de65..9b16b4b5d8bd 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -26,14 +26,12 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Optional; -import java.util.stream.Collectors; public class CareGapsOperationProvider { private static final Logger ourLog = LoggerFactory.getLogger(CareGapsOperationProvider.class); @@ -90,11 +88,7 @@ public Parameters careGapsReport( theParams.getSubject(), theParams.getStatus(), // LUKETODO: why can't we have a List @OperationParam? - theParams.getMeasureId() == null - ? null - : theParams.getMeasureId().stream() - .map(IdType::new) - .collect(Collectors.toList()), + theParams.getMeasureId(), theParams.getMeasureIdentifier(), theParams.getMeasureUrl(), Optional.ofNullable(theParams.getNonDocument()) diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index befb34d71ddc..302061179de5 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -24,11 +24,13 @@ import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.IdType; import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.StringJoiner; +import java.util.stream.Collectors; /** * Non-RequestDetails parameters for the myStatus; - private final List myMeasureId; + private final List myMeasureId; private final List myMeasureIdentifier; @@ -107,21 +109,22 @@ public CareGapsParams( myPeriodEnd = thePeriodEnd; mySubject = theSubject; myStatus = theStatus; - myMeasureId = theMeasureId; + myMeasureId = convertMeasureId(theMeasureId); myMeasureIdentifier = theMeasureIdentifier; myMeasureUrl = theMeasureUrl; myNonDocument = theNonDocument; } - private CareGapsParams(Builder builder) { - myPeriodStart = builder.myPeriodStart; - myPeriodEnd = builder.myPeriodEnd; - mySubject = builder.mySubject; - myStatus = builder.myStatus; - myMeasureId = builder.myMeasureId; - myMeasureIdentifier = builder.myMeasureIdentifier; - myMeasureUrl = builder.myMeasureUrl; - myNonDocument = builder.myNonDocument; + public CareGapsParams(CareGapsParams.Builder builder) { + this( + builder.myPeriodStart, + builder.myPeriodEnd, + builder.mySubject, + builder.myStatus, + builder.myMeasureId, + builder.myMeasureIdentifier, + builder.myMeasureUrl, + builder.myNonDocument); } public ZonedDateTime getPeriodStart() { @@ -140,7 +143,7 @@ public List getStatus() { return myStatus; } - public List getMeasureId() { + public List getMeasureId() { return myMeasureId; } @@ -197,6 +200,12 @@ public String toString() { .toString(); } + private List convertMeasureId(List theMeasureId) { + return theMeasureId == null + ? null + : theMeasureId.stream().map(IdType::new).collect(Collectors.toList()); + } + public static Builder builder() { return new Builder(); } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index e8329e65e000..8e5ad4493d14 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -24,9 +24,13 @@ import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Parameters; +import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.monad.Eithers; import java.time.ZonedDateTime; import java.util.Objects; @@ -55,7 +59,7 @@ // LUKETODO: start to integrate this with a clinical reasoning branch @EmbeddableOperationParams public class EvaluateMeasureSingleParams { - private final IdType myId; + private final Either3 myMeasure; private final ZonedDateTime myPeriodStart; @@ -96,7 +100,7 @@ public EvaluateMeasureSingleParams( @OperationParam(name = "additionalData") Bundle theAdditionalData, @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, @OperationParam(name = "parameters") Parameters theParameters) { - myId = theId; + myMeasure = Eithers.forMiddle3(theId); myPeriodStart = thePeriodStart; myPeriodEnd = thePeriodEnd; myReportType = theReportType; @@ -124,8 +128,8 @@ private EvaluateMeasureSingleParams(Builder builder) { builder.myParameters); } - public IdType getId() { - return myId; + public Either3 getMeasure() { + return myMeasure; } public ZonedDateTime getPeriodStart() { @@ -169,12 +173,12 @@ public Parameters getParameters() { } @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { + public boolean equals(Object theO) { + if (theO == null || getClass() != theO.getClass()) { return false; } - EvaluateMeasureSingleParams that = (EvaluateMeasureSingleParams) o; - return Objects.equals(myId, that.myId) + EvaluateMeasureSingleParams that = (EvaluateMeasureSingleParams) theO; + return Objects.equals(myMeasure, that.myMeasure) && Objects.equals(myPeriodStart, that.myPeriodStart) && Objects.equals(myPeriodEnd, that.myPeriodEnd) && Objects.equals(myReportType, that.myReportType) @@ -190,7 +194,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash( - myId, + myMeasure, myPeriodStart, myPeriodEnd, myReportType, @@ -206,9 +210,9 @@ public int hashCode() { @Override public String toString() { return new StringJoiner(", ", EvaluateMeasureSingleParams.class.getSimpleName() + "[", "]") - .add("myId=" + myId) - .add("myPeriodStart='" + myPeriodStart + "'") - .add("myPeriodEnd='" + myPeriodEnd + "'") + .add("myMeasure=" + myMeasure) + .add("myPeriodStart=" + myPeriodStart) + .add("myPeriodEnd=" + myPeriodEnd) .add("myReportType='" + myReportType + "'") .add("mySubject='" + mySubject + "'") .add("myPractitioner='" + myPractitioner + "'") From 4d000c4a79a719720ef6c4bbf68bf741e137e623 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Jan 2025 09:55:00 -0500 Subject: [PATCH 71/75] Fix compile and startup errors. Relax validation. --- .../rest/server/method/EmbeddedOperationUtils.java | 14 ++++++++------ .../ca/uhn/fhir/cr/r4/measure/CareGapsParams.java | 2 +- .../cr/r4/measure/MeasureOperationsProvider.java | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 370eaa444af5..060ff64a2fa8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -271,12 +271,14 @@ private static void validateGenericTypes( final Type parameterTypeArgumentAtIndex = parameterTypeArguments[index]; final Type fieldTypeArgumentAtIndex = fieldTypeArguments[index]; - if (!parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { - final String error = String.format( - "Generic type argument does not match constructor: %s, field: %s for class: %s", - parameterTypeArgumentAtIndex, fieldTypeArgumentAtIndex, theDeclaringClass); - throw new ConfigurationException(error); - } + // LUKETODO: this causes errors if we pass in a generic type with one type of generic parameter (String) + // and convert it to another (StringType) +// if (!parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { +// final String error = String.format( +// "Generic type argument does not match constructor: %s, field: %s for class: %s", +// parameterTypeArgumentAtIndex, fieldTypeArgumentAtIndex, theDeclaringClass); +// throw new ConfigurationException(error); +// } } } else { final String error = String.format( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index 302061179de5..ed8333c72ef8 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -115,7 +115,7 @@ public CareGapsParams( myNonDocument = theNonDocument; } - public CareGapsParams(CareGapsParams.Builder builder) { + private CareGapsParams(CareGapsParams.Builder builder) { this( builder.myPeriodStart, builder.myPeriodEnd, diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 6ff77dfda5c4..479ed50e3366 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -69,7 +69,7 @@ public MeasureReport evaluateMeasure( // thing as a MUTUALLY EXCLUSIVE ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE // ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation? // so 3 different params : try annotations - Eithers.forMiddle3(theParams.getId()), + theParams.getMeasure(), theParams.getPeriodStart(), theParams.getPeriodEnd(), theParams.getReportType(), From 2184de55aa19cf1ced98094a3aa08e4dbec2778a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Jan 2025 09:58:26 -0500 Subject: [PATCH 72/75] Spotless --- .../server/method/EmbeddedOperationUtils.java | 15 ++++++++------- .../cr/r4/measure/MeasureOperationsProvider.java | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 060ff64a2fa8..5d2e9973af92 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -271,14 +271,15 @@ private static void validateGenericTypes( final Type parameterTypeArgumentAtIndex = parameterTypeArguments[index]; final Type fieldTypeArgumentAtIndex = fieldTypeArguments[index]; - // LUKETODO: this causes errors if we pass in a generic type with one type of generic parameter (String) + // LUKETODO: this causes errors if we pass in a generic type with one type of generic parameter + // (String) // and convert it to another (StringType) -// if (!parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { -// final String error = String.format( -// "Generic type argument does not match constructor: %s, field: %s for class: %s", -// parameterTypeArgumentAtIndex, fieldTypeArgumentAtIndex, theDeclaringClass); -// throw new ConfigurationException(error); -// } + // if (!parameterTypeArgumentAtIndex.equals(fieldTypeArgumentAtIndex)) { + // final String error = String.format( + // "Generic type argument does not match constructor: %s, field: %s for class: %s", + // parameterTypeArgumentAtIndex, fieldTypeArgumentAtIndex, theDeclaringClass); + // throw new ConfigurationException(error); + // } } } else { final String error = String.format( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 479ed50e3366..a42a4f04eb21 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -28,7 +28,6 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; -import org.opencds.cqf.fhir.utility.monad.Eithers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From c4b3100956543d5bbde11f1dfbb415d3c346c496 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Jan 2025 11:12:44 -0500 Subject: [PATCH 73/75] Initial attempt to support the Header annotation. --- .../ca/uhn/fhir/rest/annotation/Header.java | 13 +++ .../server/method/EmbeddedOperationUtils.java | 1 + .../method/EmbeddedParameterConverter.java | 4 + .../EmbeddedParameterConverterContext.java | 14 ++-- .../rest/server/method/HeaderParameter.java | 37 ++++++++ .../fhir/rest/server/method/MethodUtil.java | 3 + .../measure/EvaluateMeasureSingleParams.java | 84 +++++++++++-------- 7 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Header.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Header.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Header.java new file mode 100644 index 000000000000..d798ae0022e0 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Header.java @@ -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(); +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java index 5d2e9973af92..1725a4b56912 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedOperationUtils.java @@ -195,6 +195,7 @@ private static void validateConstructorArgs(Constructor theConstructor, Field final Parameter[] constructorParameters = theConstructor.getParameters(); final Class[] constructorParameterTypes = theConstructor.getParameterTypes(); + // LUKETODO: if we pass in headers do we really need this validation, or do we need to tweak it? if (constructorParameterTypes.length != theDeclaredFields.length) { final String error = String.format( "%sInvalid operation embedded parameters. Constructor parameter count does not match field count: %s", diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 2d6ad8ea340f..90352ca05df0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; +import ca.uhn.fhir.rest.annotation.Header; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -107,6 +108,9 @@ private EmbeddedParameterConverterContext convertConstructorParameter(Parameter if (constructorParamAnnotation instanceof IdParam) { return EmbeddedParameterConverterContext.forParameter(new NullParameter()); + } else if (constructorParamAnnotation instanceof Header) { + return EmbeddedParameterConverterContext.forParameter( + new HeaderParameter(((Header) constructorParamAnnotation).value())); } else if (constructorParamAnnotation instanceof OperationParam) { final OperationParameter operationParameter = getOperationParameter((OperationParam) constructorParamAnnotation); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java index aac52f4b2e93..db0f2855d594 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java @@ -29,12 +29,12 @@ class EmbeddedParameterConverterContext { @Nullable - private final NullParameter myNullParameter; + private final IParameter myNonOperationParameter; @Nullable private final ParamInitializationContext myParamContext; - public static EmbeddedParameterConverterContext forParameter(NullParameter theNullParameter) { + public static EmbeddedParameterConverterContext forParameter(IParameter theNullParameter) { return new EmbeddedParameterConverterContext(theNullParameter, null); } @@ -43,15 +43,15 @@ public static EmbeddedParameterConverterContext forEmbeddedContext(ParamInitiali } private EmbeddedParameterConverterContext( - @Nullable NullParameter theNullParameter, @Nullable ParamInitializationContext theParamContext) { + @Nullable IParameter theNonOperationParameter, @Nullable ParamInitializationContext theParamContext) { - myNullParameter = theNullParameter; + myNonOperationParameter = theNonOperationParameter; myParamContext = theParamContext; } @Nullable - public NullParameter getParameter() { - return myNullParameter; + public IParameter getParameter() { + return myNonOperationParameter; } @Nullable @@ -62,7 +62,7 @@ public ParamInitializationContext getParamContext() { @Override public String toString() { return new StringJoiner(", ", EmbeddedParameterConverterContext.class.getSimpleName() + "[", "]") - .add("myNullParameter=" + myNullParameter) + .add("myNullParameter=" + myNonOperationParameter) .add("myParamContext=" + myParamContext) .toString(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java new file mode 100644 index 000000000000..e4fa9a703061 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.common.annotations.VisibleForTesting; + +import java.lang.reflect.Method; +import java.util.Collection; + +// LUKETODO: javadoc +public class HeaderParameter implements IParameter { + + private final String myHeaderName; + + public HeaderParameter(String theHeaderName) { + myHeaderName = theHeaderName; + } + + @VisibleForTesting + public String getValue() { + return myHeaderName; + } + + @Override + public Object translateQueryParametersIntoServerArgument( + RequestDetails theRequest, + BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + + return theRequest.getHeader(myHeaderName); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // LUKETODO: anything to do here? + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 0c5c664d2d49..958b4a6f8de8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; +import ca.uhn.fhir.rest.annotation.Header; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.Offset; @@ -307,6 +308,8 @@ private static IParameter getParamForNonOperationNonEmbeddedAnnotation( return createValidateNode(theContext, theNextParameterAnnotations, theParameterType); } else if (theNextAnnotation instanceof Validate.Profile) { return createValidateProfile(theContext, theNextParameterAnnotations, theParameterType); + } else if (theNextAnnotation instanceof Header) { + return new HeaderParameter(((Header) theNextAnnotation).value()); } return null; } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 8e5ad4493d14..7bd492b4af5d 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.cr.r4.measure; import ca.uhn.fhir.rest.annotation.EmbeddableOperationParams; +import ca.uhn.fhir.rest.annotation.Header; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParameterRangeType; @@ -59,6 +60,8 @@ // LUKETODO: start to integrate this with a clinical reasoning branch @EmbeddableOperationParams public class EvaluateMeasureSingleParams { + // LUKETODO: not sure if we need this but keep it for now + private final String myTimezone; private final Either3 myMeasure; private final ZonedDateTime myPeriodStart; @@ -84,6 +87,7 @@ public class EvaluateMeasureSingleParams { // LUKETODO: embedded factory constructor annoation // LUKETODO: annotations on constructor parameters instead public EvaluateMeasureSingleParams( + @Header("Timezone") String theTimezone, @IdParam IdType theId, @OperationParam( name = "periodStart", @@ -100,6 +104,7 @@ public EvaluateMeasureSingleParams( @OperationParam(name = "additionalData") Bundle theAdditionalData, @OperationParam(name = "terminologyEndpoint") Endpoint theTerminologyEndpoint, @OperationParam(name = "parameters") Parameters theParameters) { + myTimezone = theTimezone; myMeasure = Eithers.forMiddle3(theId); myPeriodStart = thePeriodStart; myPeriodEnd = thePeriodEnd; @@ -113,19 +118,24 @@ public EvaluateMeasureSingleParams( myParameters = theParameters; } - private EvaluateMeasureSingleParams(Builder builder) { + private EvaluateMeasureSingleParams(EvaluateMeasureSingleParams.Builder builder) { this( - builder.myId, - builder.myPeriodStart, - builder.myPeriodEnd, - builder.myReportType, - builder.mySubject, - builder.myPractitioner, - builder.myLastReceivedOn, - builder.myProductLine, - builder.myAdditionalData, - builder.myTerminologyEndpoint, - builder.myParameters); + builder.myTimezone, + builder.myId, + builder.myPeriodStart, + builder.myPeriodEnd, + builder.myReportType, + builder.mySubject, + builder.myPractitioner, + builder.myLastReceivedOn, + builder.myProductLine, + builder.myAdditionalData, + builder.myTerminologyEndpoint, + builder.myParameters); + } + + public String getTimezone() { + return myTimezone; } public Either3 getMeasure() { @@ -229,6 +239,7 @@ public static Builder builder() { } public static class Builder { + private String myTimezone; private IdType myId; private ZonedDateTime myPeriodStart; private ZonedDateTime myPeriodEnd; @@ -240,59 +251,64 @@ public static class Builder { private Bundle myAdditionalData; private Endpoint myTerminologyEndpoint; private Parameters myParameters; + + public Builder setTimezone(String theTimezone) { + myTimezone = theTimezone; + return this; + } - public Builder setId(IdType myId) { - this.myId = myId; + public Builder setId(IdType theId) { + myId = theId; return this; } - public Builder setPeriodStart(ZonedDateTime myPeriodStart) { - this.myPeriodStart = myPeriodStart; + public Builder setPeriodStart(ZonedDateTime thePeriodStart) { + myPeriodStart = thePeriodStart; return this; } - public Builder setPeriodEnd(ZonedDateTime myPeriodEnd) { - this.myPeriodEnd = myPeriodEnd; + public Builder setPeriodEnd(ZonedDateTime thePeriodEnd) { + myPeriodEnd = thePeriodEnd; return this; } - public Builder setReportType(String myReportType) { - this.myReportType = myReportType; + public Builder setReportType(String theReportType) { + myReportType = theReportType; return this; } - public Builder setSubject(String mySubject) { - this.mySubject = mySubject; + public Builder setSubject(String theSubject) { + mySubject = theSubject; return this; } - public Builder setPractitioner(String myPractitioner) { - this.myPractitioner = myPractitioner; + public Builder setPractitioner(String thePractitioner) { + myPractitioner = thePractitioner; return this; } - public Builder setLastReceivedOn(String myLastReceivedOn) { - this.myLastReceivedOn = myLastReceivedOn; + public Builder setLastReceivedOn(String theLastReceivedOn) { + myLastReceivedOn = theLastReceivedOn; return this; } - public Builder setProductLine(String myProductLine) { - this.myProductLine = myProductLine; + public Builder setProductLine(String theProductLine) { + myProductLine = theProductLine; return this; } - public Builder setAdditionalData(Bundle myAdditionalData) { - this.myAdditionalData = myAdditionalData; + public Builder setAdditionalData(Bundle theAdditionalData) { + myAdditionalData = theAdditionalData; return this; } - public Builder setTerminologyEndpoint(Endpoint myTerminologyEndpoint) { - this.myTerminologyEndpoint = myTerminologyEndpoint; + public Builder setTerminologyEndpoint(Endpoint theTerminologyEndpoint) { + myTerminologyEndpoint = theTerminologyEndpoint; return this; } - public Builder setParameters(Parameters myParameters) { - this.myParameters = myParameters; + public Builder setParameters(Parameters theParameters) { + myParameters = theParameters; return this; } From 146ea0e3d9ebeddf1fd35156277ef21b2794fe0a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Jan 2025 11:16:12 -0500 Subject: [PATCH 74/75] Spotless. --- .../method/EmbeddedParameterConverter.java | 2 +- .../EmbeddedParameterConverterContext.java | 2 +- .../rest/server/method/HeaderParameter.java | 10 +++++-- .../measure/EvaluateMeasureSingleParams.java | 28 +++++++++---------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index 90352ca05df0..3b64b5231aaa 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -110,7 +110,7 @@ private EmbeddedParameterConverterContext convertConstructorParameter(Parameter return EmbeddedParameterConverterContext.forParameter(new NullParameter()); } else if (constructorParamAnnotation instanceof Header) { return EmbeddedParameterConverterContext.forParameter( - new HeaderParameter(((Header) constructorParamAnnotation).value())); + new HeaderParameter(((Header) constructorParamAnnotation).value())); } else if (constructorParamAnnotation instanceof OperationParam) { final OperationParameter operationParameter = getOperationParameter((OperationParam) constructorParamAnnotation); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java index db0f2855d594..1a1b0e3aedeb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverterContext.java @@ -43,7 +43,7 @@ public static EmbeddedParameterConverterContext forEmbeddedContext(ParamInitiali } private EmbeddedParameterConverterContext( - @Nullable IParameter theNonOperationParameter, @Nullable ParamInitializationContext theParamContext) { + @Nullable IParameter theNonOperationParameter, @Nullable ParamInitializationContext theParamContext) { myNonOperationParameter = theNonOperationParameter; myParamContext = theParamContext; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java index e4fa9a703061..c9001f958582 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HeaderParameter.java @@ -24,14 +24,18 @@ public String getValue() { @Override public Object translateQueryParametersIntoServerArgument( - RequestDetails theRequest, - BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + RequestDetails theRequest, BaseMethodBinding theMethodBinding) + throws InternalErrorException, InvalidRequestException { return theRequest.getHeader(myHeaderName); } @Override - public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + public void initializeTypes( + Method theMethod, + Class> theOuterCollectionType, + Class> theInnerCollectionType, + Class theParameterType) { // LUKETODO: anything to do here? } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 7bd492b4af5d..209abba49e3c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -87,7 +87,7 @@ public class EvaluateMeasureSingleParams { // LUKETODO: embedded factory constructor annoation // LUKETODO: annotations on constructor parameters instead public EvaluateMeasureSingleParams( - @Header("Timezone") String theTimezone, + @Header("Timezone") String theTimezone, @IdParam IdType theId, @OperationParam( name = "periodStart", @@ -120,18 +120,18 @@ public EvaluateMeasureSingleParams( private EvaluateMeasureSingleParams(EvaluateMeasureSingleParams.Builder builder) { this( - builder.myTimezone, - builder.myId, - builder.myPeriodStart, - builder.myPeriodEnd, - builder.myReportType, - builder.mySubject, - builder.myPractitioner, - builder.myLastReceivedOn, - builder.myProductLine, - builder.myAdditionalData, - builder.myTerminologyEndpoint, - builder.myParameters); + builder.myTimezone, + builder.myId, + builder.myPeriodStart, + builder.myPeriodEnd, + builder.myReportType, + builder.mySubject, + builder.myPractitioner, + builder.myLastReceivedOn, + builder.myProductLine, + builder.myAdditionalData, + builder.myTerminologyEndpoint, + builder.myParameters); } public String getTimezone() { @@ -251,7 +251,7 @@ public static class Builder { private Bundle myAdditionalData; private Endpoint myTerminologyEndpoint; private Parameters myParameters; - + public Builder setTimezone(String theTimezone) { myTimezone = theTimezone; return this; From 037ffd7606d70d4b5141bc21cf21ef89690c0315 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Jan 2025 11:46:21 -0500 Subject: [PATCH 75/75] Add test cases for @Header. --- ...thodBindingMethodParameterBuilderTest.java | 48 ++++++++++++++++--- ...OperationParamsInnerClassesAndMethods.java | 6 +++ .../rest/server/method/MethodUtilTest.java | 17 +++++-- .../method/OperationMethodBindingTest.java | 2 +- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java index 9d6277fee2fe..2c203ccc052f 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilderTest.java @@ -4,11 +4,16 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.PatientProvider; import ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SampleParams; +import jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.IdType; -import org.junit.jupiter.api.Disabled; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; @@ -18,6 +23,8 @@ import java.time.ZoneOffset; import java.util.List; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.EXPAND; +import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.OP_INSTANCE_OR_TYPE; import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithTypeConversion; import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.ParamsWithoutAnnotations; import static ca.uhn.fhir.rest.server.method.MethodAndOperationParamsInnerClassesAndMethods.SAMPLE_METHOD_EMBEDDED_TYPE_MULTIPLE_REQUEST_DETAILS; @@ -41,6 +48,7 @@ class BaseMethodBindingMethodParameterBuilderTest { private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(BaseMethodBindingMethodParameterBuilderTest.class); private static final RequestDetails REQUEST_DETAILS = new SystemRequestDetails(); + private static final String TIMEZONE_AMERICA_TORONTO = "America/Toronto"; private final MethodAndOperationParamsInnerClassesAndMethods myMethodAndOperationParamsInnerClassesAndMethods = new MethodAndOperationParamsInnerClassesAndMethods(); @@ -114,12 +122,42 @@ void happyPathOperationEmbeddedTypesRequestDetailsLast() { } @Test - @Disabled void happyPathOperationEmbeddedTypesWithIdType() { final IdType id = new IdType(); final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_EMBEDDED_TYPE_REQUEST_DETAILS_FIRST_WITH_ID_TYPE, RequestDetails.class, SampleParamsWithIdParam.class); - final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, "param1", List.of("param2"), new BooleanType(false)}; - final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, "param1", List.of("param2"), new BooleanType(false))}; + final Object[] inputParams = new Object[]{REQUEST_DETAILS, id, TIMEZONE_AMERICA_TORONTO, "param1", List.of("param2"), new BooleanType(false)}; + final Object[] expectedOutputParams = new Object[]{REQUEST_DETAILS, new SampleParamsWithIdParam(id, TIMEZONE_AMERICA_TORONTO, "param1", List.of("param2"), new BooleanType(false))}; + + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + @Test + void expand() { + final IdType id = new IdType(); + final ValueSet valueSet = new ValueSet(); + + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(EXPAND, HttpServletRequest.class, IIdType.class, IBaseResource.class, RequestDetails.class); + final TestHttpServletRequest testHttpServletRequest = new TestHttpServletRequest(); + final Object[] inputParams = new Object[]{testHttpServletRequest, id, TIMEZONE_AMERICA_TORONTO, valueSet, REQUEST_DETAILS}; + final Object[] expectedOutputParams = new Object[]{testHttpServletRequest, id, TIMEZONE_AMERICA_TORONTO, valueSet, REQUEST_DETAILS}; + + final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); + + assertArrayEquals(expectedOutputParams, actualOutputParams); + } + + @Test + void opInstanceOrType() { + final IdType id = new IdType(); + final PatientProvider provider = new PatientProvider(); + final StringType stringType = new StringType("stringType"); + final Patient patient = new Patient(); + + final Method sampleMethod = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(provider, OP_INSTANCE_OR_TYPE, IdType.class, String.class, StringType.class, Patient.class); + final Object[] inputParams = new Object[]{id, TIMEZONE_AMERICA_TORONTO, stringType, patient}; + final Object[] expectedOutputParams = new Object[]{id, TIMEZONE_AMERICA_TORONTO, stringType, patient}; final Object[] actualOutputParams = buildMethodParams(sampleMethod, REQUEST_DETAILS, inputParams); @@ -157,9 +195,7 @@ void buildMethodParams_multipleRequestDetails_shouldThrowInternalErrorException( }); } - // LUKETODO: decide what to do with this @Test - @Disabled void buildMethodParams_withClassMiissingParameterAnnotations_shouldThrowInternalErrorException() { final Method method = myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(SAMPLE_METHOD_PARAM_NO_EMBEDDED_TYPE, ParamsWithoutAnnotations.class); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java index bef6da4216d4..f974ed2b5c66 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.annotation.EmbeddedOperationParams; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; +import ca.uhn.fhir.rest.annotation.Header; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Offset; import ca.uhn.fhir.rest.annotation.Operation; @@ -303,6 +304,8 @@ public String toString() { static class SampleParamsWithIdParam { private final IdType myId; + private final String myTimezone; + private final String myParam1; private final List myParam2; @@ -312,6 +315,7 @@ static class SampleParamsWithIdParam { public SampleParamsWithIdParam( @IdParam IdType theId, + @Header("Timezone") String theTimezone, @OperationParam(name = "param1") String theParam1, @OperationParam(name = "param2") @@ -319,6 +323,7 @@ public SampleParamsWithIdParam( @OperationParam(name = "param3") BooleanType theParam3) { myId = theId; + myTimezone = theTimezone; myParam1 = theParam1; myParam2 = theParam2; myParam3 = theParam3; @@ -499,6 +504,7 @@ public Class getResourceType() { @Operation(name = "$OP_INSTANCE_OR_TYPE") Parameters opInstanceOrType( @IdParam(optional = true) IdType theId, + @Header("Timezone") String theTimezone, @OperationParam(name = "PARAM1" ) StringType theParam1, @OperationParam(name = "PARAM2" ) Patient theParam2) { return new Parameters(); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 752ca7266f3a..041e1289a1c9 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -167,14 +167,15 @@ void expand() { @Test void opInstanceOrType() { myProvider = new MethodAndOperationParamsInnerClassesAndMethods.PatientProvider(); - final List resourceParameters = getMethodAndExecute(OP_INSTANCE_OR_TYPE, IdType.class, StringType.class, Patient.class); + final List resourceParameters = getMethodAndExecute(OP_INSTANCE_OR_TYPE, IdType.class, String.class, StringType.class, Patient.class); assertThat(resourceParameters).isNotNull() .isNotEmpty() - .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class); + .hasExactlyElementsOfTypes(NullParameter.class, HeaderParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), + new HeaderParameterToAssert("Timezone"), new OperationParameterToAssert(ourFhirContext, "PARAM1", "$OP_INSTANCE_OR_TYPE", null, String.class, "string", Void.class, OperationParameterRangeType.NOT_APPLICABLE), new OperationParameterToAssert(ourFhirContext, "PARAM2", "$OP_INSTANCE_OR_TYPE", null, String.class, "Patient", Void.class, OperationParameterRangeType.NOT_APPLICABLE) ); @@ -633,10 +634,11 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { assertThat(resourceParameters) .isNotNull() .isNotEmpty() - .hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); + .hasExactlyElementsOfTypes(NullParameter.class, HeaderParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); final List expectedParameters = List.of( new NullParameterToAssert(), + new HeaderParameterToAssert("Timezone"), new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, null, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, ArrayList.class, String.class, null, Void.class, OperationParameterRangeType.NOT_APPLICABLE), new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS_WITH_ID_TYPE, null, String.class, "boolean", Void.class, OperationParameterRangeType.NOT_APPLICABLE) @@ -724,6 +726,12 @@ private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, I return true; } + if (theExpectedParameter instanceof HeaderParameterToAssert && theActualParameter instanceof HeaderParameter) { + assertThat(((HeaderParameter) theActualParameter).getValue()).isEqualTo(((HeaderParameterToAssert) theExpectedParameter).myHeaderValue()); + + return true; + } + if (theExpectedParameter instanceof ServletRequestParameterToAssert && theActualParameter instanceof ServletRequestParameter) { return true; } @@ -748,6 +756,9 @@ private interface IParameterToAssert {} private record NullParameterToAssert() implements IParameterToAssert { } + private record HeaderParameterToAssert(String myHeaderValue) implements IParameterToAssert { + } + private record ServletRequestParameterToAssert() implements IParameterToAssert { } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java index 83447b0d1e15..2989cd4c8ebb 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/OperationMethodBindingTest.java @@ -148,7 +148,7 @@ void expandEnsureMethodEnsureCanOperateAtTypeLevel() { @Test void methodWithIdParamButNoIIdType() { myProvider = new PatientProvider(); - init(OP_INSTANCE_OR_TYPE, IdType.class, StringType.class, Patient.class); + init(OP_INSTANCE_OR_TYPE, IdType.class, String.class, StringType.class, Patient.class); final SystemRequestDetails requestDetails = new SystemRequestDetails(); requestDetails.setRequestType(RequestTypeEnum.POST);