From d9d3bba8ebeb889f8a0a34eb04583293583b4bc6 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sat, 25 Jan 2025 23:53:07 -0500 Subject: [PATCH 1/4] First commit: Refactored MethodUtil. No tests. --- .../fhir/rest/server/method/MethodUtil.java | 739 +++++++++++------- 1 file changed, 478 insertions(+), 261 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 e3c12c79caa..71f0eb4ed97 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 @@ -62,6 +62,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; @@ -113,58 +115,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(theMethod, paramIndex); - if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { - try { - theMethod = theMethod - .getDeclaringClass() - .getSuperclass() - .getMethod(theMethod.getName(), parameterTypes); - parameterType = - 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() + "'"); - } - } - declaredParameterType = parameterType; - } - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); - declaredParameterType = parameterType; - } - 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)"); - } + final GenericsContext genericsContext = + getGenericsContext(theContext, theMethod, parameterTypes, paramIndex); - /* - * 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, paramIndex); - if (Date.class.equals(genericType)) { - BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("dateTime"); - parameterType = dateTimeDef.getImplementingClass(); - } else if (String.class.equals(genericType) || genericType == null) { - BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); - parameterType = dateTimeDef.getImplementingClass(); - } - } + parameterType = genericsContext.getParameterType(); + declaredParameterType = genericsContext.getDeclaredParameterType(); + outerCollectionType = genericsContext.getOuterCollectionType(); + innerCollectionType = genericsContext.getInnerCollectionType(); } if (ServletRequest.class.isAssignableFrom(parameterType)) { @@ -186,217 +143,20 @@ public static List getResourceParameters( param = new SearchTotalModeParameter(); } else { 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; - } 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; - } 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 '" + theMethod.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() + "'"); - specType = parameterType; - } - - param = new IncludeParameter( - (IncludeParam) nextAnnotation, instantiableCollectionType, specType); - } 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(theMethod.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 = theMethod.getAnnotation(Operation.class) != null; - boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null; - param = new ResourceParameter( - (Class) parameterType, - theProvider, - mode, - methodIsOperation, - methodIsPatch); - } 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 = new SinceParameter(); - ((SinceParameter) param) - .setType(theContext, parameterType, innerCollectionType, outerCollectionType); - } else if (nextAnnotation instanceof At) { - param = new AtParameter(); - ((AtParameter) param) - .setType(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) { - 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 ParameterContext parameterContext = handleAnnotation( + theContext, + theProvider, + theMethod, + nextParameterAnnotations, + nextParameterAnnotations[i], + parameterType, + declaredParameterType, + outerCollectionType, + innerCollectionType, + parameters); - OperationParam operationParam = (OperationParam) nextAnnotation; - String description = ParametersUtil.extractDescription(nextParameterAnnotations); - List examples = ParametersUtil.extractExamples(nextParameterAnnotations); - ; - param = new OperationParameter( - theContext, - op.name(), - operationParam.name(), - operationParam.min(), - operationParam.max(), - description, - examples); - 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 " + theMethod); - } - parameterType = newParameterType; - } - } else if (nextAnnotation instanceof Validate.Mode) { - if (parameterType.equals(ValidationModeEnum.class) == false) { - 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) - .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()); - } - }); - } else if (nextAnnotation instanceof Validate.Profile) { - if (parameterType.equals(String.class) == false) { - 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) - .setConverter(new IOperationParamConverter() { - @Override - public Object incomingServer(Object theObject) { - return theObject.toString(); - } - - @Override - public Object outgoingClient(Object theObject) { - return ParametersUtil.createString(theContext, theObject.toString()); - } - }); - } else { - continue; - } + param = parameterContext.getParam(); + parameterType = parameterContext.getParameterType(); } } @@ -415,4 +175,461 @@ public Object outgoingClient(Object theObject) { } return parameters; } + + @Nonnull + private static ParameterContext handleAnnotation( + FhirContext theContext, + Object theProvider, + Method theMethod, + Annotation[] theNextParameterAnnotations, + Annotation theNextAnnotation, + Class theParameterType, + Class theDeclaredParameterType, + Class> theOuterCollectionType, + Class> theInnerCollectionType, + List parameters) { + if (theNextAnnotation instanceof RequiredParam) { + return new ParameterContext( + theParameterType, + createRequiredParam( + theContext, + theNextParameterAnnotations, + (RequiredParam) theNextAnnotation, + theParameterType, + theInnerCollectionType, + theOuterCollectionType)); + } else if (theNextAnnotation instanceof OptionalParam) { + return new ParameterContext( + theParameterType, + createOptionalParam( + theContext, + theNextParameterAnnotations, + (OptionalParam) theNextAnnotation, + theParameterType, + theInnerCollectionType, + theOuterCollectionType)); + } else if (theNextAnnotation instanceof RawParam) { + return new ParameterContext(theParameterType, new RawParamsParameter(parameters)); + } else if (theNextAnnotation instanceof IncludeParam) { + return new ParameterContext( + theParameterType, + createIncludeParam( + theMethod, theParameterType, theInnerCollectionType, theOuterCollectionType, (IncludeParam) + theNextAnnotation)); + } else if (theNextAnnotation instanceof ResourceParam) { + return new ParameterContext( + theParameterType, createResourceParam(theMethod, theProvider, theParameterType)); + } else if (theNextAnnotation instanceof IdParam) { + return new ParameterContext(theParameterType, new NullParameter()); + } else if (theNextAnnotation instanceof ServerBase) { + return new ParameterContext(theParameterType, new ServerBaseParamBinder()); + } else if (theNextAnnotation instanceof Elements) { + return new ParameterContext(theParameterType, new ElementsParameter()); + } else if (theNextAnnotation instanceof Since) { + return new ParameterContext( + theParameterType, + createSinceParameter(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType)); + } else if (theNextAnnotation instanceof At) { + return new ParameterContext( + theParameterType, + createAtParameter(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType)); + } else if (theNextAnnotation instanceof Count) { + return new ParameterContext(theParameterType, new CountParameter()); + } else if (theNextAnnotation instanceof Offset) { + return new ParameterContext(theParameterType, new OffsetParameter()); + } else if (theNextAnnotation instanceof GraphQLQueryUrl) { + return new ParameterContext(theParameterType, new GraphQLQueryUrlParameter()); + } else if (theNextAnnotation instanceof GraphQLQueryBody) { + return new ParameterContext(theParameterType, new GraphQLQueryBodyParameter()); + } else if (theNextAnnotation instanceof Sort) { + return new ParameterContext(theParameterType, new SortParameter(theContext)); + } else if (theNextAnnotation instanceof TransactionParam) { + return new ParameterContext(theParameterType, new TransactionParameter(theContext)); + } else if (theNextAnnotation instanceof ConditionalUrlParam) { + return new ParameterContext( + theParameterType, + new ConditionalParamBinder(((ConditionalUrlParam) theNextAnnotation).supportsMultiple())); + } else if (theNextAnnotation instanceof OperationParam) { + return createOperationParameterContext( + theContext, + theMethod, + theNextParameterAnnotations, + theNextAnnotation, + theParameterType, + theDeclaredParameterType); + } else if (theNextAnnotation instanceof Validate.Mode) { + return new ParameterContext( + theParameterType, createValidateNode(theContext, theNextParameterAnnotations, theParameterType)); + } else if (theNextAnnotation instanceof Validate.Profile) { + return new ParameterContext( + theParameterType, createValidateProfile(theContext, theNextParameterAnnotations, theParameterType)); + } + + return new ParameterContext(theParameterType, null); + } + + @Nonnull + private static ParameterContext createOperationParameterContext( + FhirContext theContext, + Method theMethod, + Annotation[] theNextParameterAnnotations, + Annotation theNextAnnotation, + Class theParameterType, + Class theDeclaredParameterType) { + 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()); + } + + OperationParam operationParam = (OperationParam) theNextAnnotation; + String description = ParametersUtil.extractDescription(theNextParameterAnnotations); + List examples = ParametersUtil.extractExamples(theNextParameterAnnotations); + Class parameterTypeInner = theParameterType; + + OperationParameter param = new OperationParameter( + theContext, + op.name(), + operationParam.name(), + operationParam.min(), + operationParam.max(), + description, + examples); + 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); + } + + @Nonnull + private static IParameter createAtParameter( + FhirContext theContext, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + IParameter param; + param = new AtParameter(); + ((AtParameter) param).setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + return param; + } + + @Nonnull + private static IParameter createSinceParameter( + FhirContext theTheContext, + Class theParameterType, + Class> theInnerCollectionType, + Class> theOuterCollectionType) { + IParameter param; + param = new SinceParameter(); + ((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 = + (Class>) 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 { + StringBuilder b = new StringBuilder(); + b.append("Method '"); + b.append(theMethod.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 = theMethod.getAnnotation(Operation.class) != null; + boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null; + param = new ResourceParameter( + (Class) 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) + .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) + .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; + } + + 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 = (Class>) 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 (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = (Class>) parameterType; + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); + declaredParameterType = parameterType; + } + if (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 = dateTimeDef.getImplementingClass(); + } else if (String.class.equals(genericType) || genericType == null) { + BaseRuntimeElementDefinition dateTimeDef = theContext.getElementDefinition("string"); + parameterType = dateTimeDef.getImplementingClass(); + } + } + + return new GenericsContext(parameterType, declaredParameterType, outerCollectionType, innerCollectionType); + } + + 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; + } + } + + 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; + } + } } From f4b0bc1cb97956913a87fde00219951ac4b9ca47 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Sun, 26 Jan 2025 11:34:55 -0500 Subject: [PATCH 2/4] Spotless. More refactor/cleanup. Add simple tests. --- .../fhir/rest/server/method/MethodUtil.java | 57 ++++--- .../EmbeddedParamsInnerClassesAndMethods.java | 156 ++++++++++++++++++ .../rest/server/method/MethodUtilTest.java | 134 +++++++++++++++ 3 files changed, 320 insertions(+), 27 deletions(-) create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.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 71f0eb4ed97..7694f64de42 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 @@ -75,6 +75,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; @@ -97,7 +98,6 @@ public static void extractDescription(SearchParameter theParameter, Annotation[] } } - @SuppressWarnings("unchecked") public static List getResourceParameters( final FhirContext theContext, Method theMethod, Object theProvider) { List parameters = new ArrayList<>(); @@ -324,9 +324,8 @@ private static IParameter createAtParameter( Class theParameterType, Class> theInnerCollectionType, Class> theOuterCollectionType) { - IParameter param; - param = new AtParameter(); - ((AtParameter) param).setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + final AtParameter param = new AtParameter(); + param.setType(theContext, theParameterType, theInnerCollectionType, theOuterCollectionType); return param; } @@ -336,10 +335,8 @@ private static IParameter createSinceParameter( Class theParameterType, Class> theInnerCollectionType, Class> theOuterCollectionType) { - IParameter param; - param = new SinceParameter(); - ((SinceParameter) param) - .setType(theTheContext, theParameterType, theInnerCollectionType, theOuterCollectionType); + final SinceParameter param = new SinceParameter(); + param.setType(theTheContext, theParameterType, theInnerCollectionType, theOuterCollectionType); return param; } @@ -406,9 +403,8 @@ private static IParameter createIncludeParam( + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" + Include.class.getSimpleName() + ">"); } else { - instantiableCollectionType = - (Class>) CollectionBinder.getInstantiableCollectionType( - theInnerCollectionType, "Method '" + theMethod.getName() + "'"); + instantiableCollectionType = unsafeCast(CollectionBinder.getInstantiableCollectionType( + theInnerCollectionType, "Method '" + theMethod.getName() + "'")); specType = theParameterType; } @@ -429,20 +425,18 @@ private static IParameter createResourceParam(Method theMethod, Object theProvid } else if (EncodingEnum.class.equals(theParameterType)) { mode = Mode.ENCODING; } else { - StringBuilder b = new StringBuilder(); - b.append("Method '"); - b.append(theMethod.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()); + 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( - (Class) theParameterType, theProvider, mode, methodIsOperation, methodIsPatch); + unsafeCast(theParameterType), theProvider, mode, methodIsOperation, methodIsPatch); return param; } @@ -526,7 +520,7 @@ private static GenericsContext getGenericsContext( Class> innerCollectionType = null; if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = (Class>) parameterType; + innerCollectionType = unsafeCast(parameterType); parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) { try { @@ -543,13 +537,13 @@ private static GenericsContext getGenericsContext( } declaredParameterType = parameterType; } - if (Collection.class.isAssignableFrom(parameterType)) { + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; + innerCollectionType = unsafeCast(parameterType); parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); declaredParameterType = parameterType; } - if (Collection.class.isAssignableFrom(parameterType)) { + if (parameterType != null && Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + theParamIndex + " of Method '" + theMethod.getName() + "' in type '" @@ -569,16 +563,25 @@ private static GenericsContext getGenericsContext( Class genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, theParamIndex); 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); } } return new GenericsContext(parameterType, declaredParameterType, outerCollectionType, innerCollectionType); } + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T) theObject; + } + private static class GenericsContext { private final Class parameterType; private final Class declaredParameterType; 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 new file mode 100644 index 00000000000..ff6ae767be3 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java @@ -0,0 +1,156 @@ +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.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; +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.Parameters; +import org.hl7.fhir.r4.model.Patient; +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; +import java.util.StringJoiner; + +import static org.junit.jupiter.api.Assertions.fail; + +// 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 { + + 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"; + 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"; + 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"; + + + 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 theContainingClass.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 + 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() { + } + + @Operation(name = "") + void invalidOperation() { + + } + + @Operation(name = "$simpleOperation") + void simpleOperation() { + + } + + @Operation(name = "$withEmbeddedParams") + void withEmbeddedParams() { + + } + + void invalidMethodOperationParamsNoOperationInvalid( + @OperationParam(name = "param1") String theParam1) { + + } + + @Operation(name="sampleMethodOperationParams", type = Measure.class) + public MeasureReport sampleMethodOperationParams( + @IdParam IIdType 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); + } + + @Operation(name = "$expand", idempotent = true, typeName = "ValueSet") + 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 new file mode 100644 index 00000000000..d5395d63331 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -0,0 +1,134 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +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.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; + +// 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 Object myProvider = new Object(); + + @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 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), + 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; + } + + 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 { + } +} From e44aaf0b997d55ff25f1042564c9cd12c132263a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 27 Jan 2025 17:27:05 -0500 Subject: [PATCH 3/4] Add lots more tests and achieve 80% code coverage. --- .../server/method/OperationParameter.java | 11 + .../EmbeddedParamsInnerClassesAndMethods.java | 156 ------ ...OperationParamsInnerClassesAndMethods.java | 339 ++++++++++++ .../rest/server/method/MethodUtilTest.java | 498 +++++++++++++++++- 4 files changed, 833 insertions(+), 171 deletions(-) delete mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java 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 13f11945e3f..66dad3db035 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; @@ -156,6 +157,16 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + + @VisibleForTesting + public String getOperationName() { + return myOperationName; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( 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 deleted file mode 100644 index ff6ae767be3..00000000000 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/EmbeddedParamsInnerClassesAndMethods.java +++ /dev/null @@ -1,156 +0,0 @@ -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.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; -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.Parameters; -import org.hl7.fhir.r4.model.Patient; -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; -import java.util.StringJoiner; - -import static org.junit.jupiter.api.Assertions.fail; - -// 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 { - - 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"; - 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"; - 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"; - - - 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 theContainingClass.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 - 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() { - } - - @Operation(name = "") - void invalidOperation() { - - } - - @Operation(name = "$simpleOperation") - void simpleOperation() { - - } - - @Operation(name = "$withEmbeddedParams") - void withEmbeddedParams() { - - } - - void invalidMethodOperationParamsNoOperationInvalid( - @OperationParam(name = "param1") String theParam1) { - - } - - @Operation(name="sampleMethodOperationParams", type = Measure.class) - public MeasureReport sampleMethodOperationParams( - @IdParam IIdType 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); - } - - @Operation(name = "$expand", idempotent = true, typeName = "ValueSet") - 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/MethodAndOperationParamsInnerClassesAndMethods.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java new file mode 100644 index 00000000000..c1c1227b73d --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodAndOperationParamsInnerClassesAndMethods.java @@ -0,0 +1,339 @@ +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.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.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.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; +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.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; + +// 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 MethodAndOperationParamsInnerClassesAndMethods { + + 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 SIMPLE_OPERATION = "simpleOperation"; + static final String SUPER_SIMPLE = "superSimple"; + static final String INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION = "invalidMethodOperationParamsNoOperationInvalid"; + static final String SAMPLE_METHOD_OPERATION_PARAMS = "sampleMethodOperationParams"; + 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 theContainingClass.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 + 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() { + // 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) + public MeasureReport sampleMethodOperationParams( + @IdParam IIdType 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); + } + + @Operation(name = "$expand", idempotent = true, typeName = "ValueSet") + 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(); + } + } + + // 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 d5395d63331..6b49541b366 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,36 +2,72 @@ 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.api.ValidationModeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +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 org.mockito.Mockito; +import java.lang.annotation.Annotation; +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.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_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_OPERATION_PARAMS; +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 @@ -44,43 +80,458 @@ void invalid_methodWithOperationParamsNoOperation() { @Test void invalidMethodWithNoAnnotations() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithNoAnnotations", String.class)) + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_NO_ANNOTATIONS, String.class)) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("has no recognized FHIR interface parameter nextParameterAnnotations"); } @Test void invalidMethodWithInvalidGenericType() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithInvalidGenericType", List.class)) + assertThatThrownBy(() -> getMethodAndExecute(METHOD_WITH_INVALID_GENERIC_TYPE, List.class)) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("is of an invalid generic type"); } @Test void invalidMethodWithUnknownTypeName() { - assertThatThrownBy(() -> getMethodAndExecute("methodWithUnknownTypeName", String.class)) + 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("methodWithNonAssignableTypeName", String.class)) + 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("methodWithInvalidAnnotation", String.class)) + 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).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); + + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_OPERATION_PARAMS, ArrayList.class, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_OPERATION_PARAMS, null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "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"), + 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"), + new OperationParameterToAssert(ourFhirContext, "PARAM2", "$OP_INSTANCE_OR_TYPE", null, String.class, "Patient") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); + } + + // ------------------------------------------------------------------------- + // 1. Testing MethodUtil.extractDescription() + // ------------------------------------------------------------------------- + @Test + void testExtractDescription_withDescriptionAnnotation() { + SearchParameter parameter = new SearchParameter(); + Description descriptionAnnotation = Mockito.mock(Description.class); + Mockito.when(descriptionAnnotation.shortDefinition()).thenReturn("Test description"); + + Annotation[] annotations = new Annotation[]{ descriptionAnnotation }; + MethodUtil.extractDescription(parameter, annotations); + + assertEquals("Test description", parameter.getDescription()); + } + + @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()); + assertTrue(params.get(0) instanceof SortParameter); + } + + @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)); + } + private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( ourFhirContext, - myEmbeddedParamsInnerClassesAndMethods.getDeclaredMethod(theMethodName, theParamClasses), + myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, theMethodName, theParamClasses), myProvider); } @@ -111,6 +562,20 @@ 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()); + assertThat(actualOperationParameter.getName()).isEqualTo(expectedOperationParameter.myName()); + assertThat(actualOperationParameter.getParamType()).isEqualTo(expectedOperationParameter.myParamType()); + assertThat(actualOperationParameter.getInnerCollectionType()).isEqualTo(expectedOperationParameter.myInnerCollectionType()); + + return true; + } + return false; } @@ -119,6 +584,9 @@ private interface IParameterToAssert {} private record NullParameterToAssert() implements IParameterToAssert { } + private record ServletRequestParameterToAssert() implements IParameterToAssert { + } + private record RequestDetailsParameterToAssert() implements IParameterToAssert { } From c6a5ceb77a9e87cf94dc9d45a5e71c9ebc6bc562 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 28 Jan 2025 10:11:12 -0500 Subject: [PATCH 4/4] Get rid of mockito. --- ...OperationParamsInnerClassesAndMethods.java | 10 +++++++ .../rest/server/method/MethodUtilTest.java | 26 +++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) 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 c1c1227b73d..c2a3b77dc74 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 @@ -2,6 +2,7 @@ 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; @@ -54,6 +55,7 @@ // Methods and embedded param classes to be used for testing regression code in hapi-fhir-server method classes 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"; @@ -168,6 +170,14 @@ Parameters opInstanceOrType( } } + @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 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 6b49541b366..f2c94b8412c 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 @@ -24,9 +24,9 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Date; @@ -34,6 +34,7 @@ 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; @@ -176,14 +177,19 @@ void opInstanceOrType() { // ------------------------------------------------------------------------- @Test void testExtractDescription_withDescriptionAnnotation() { - SearchParameter parameter = new SearchParameter(); - Description descriptionAnnotation = Mockito.mock(Description.class); - Mockito.when(descriptionAnnotation.shortDefinition()).thenReturn("Test description"); + final Method method = getMethod(METHOD_WITH_DESCRIPTION); + final Annotation[] annotations = method.getAnnotations(); - Annotation[] annotations = new Annotation[]{ descriptionAnnotation }; - MethodUtil.extractDescription(parameter, annotations); + assertThat(annotations.length).isEqualTo(1); + + final Annotation annotation = annotations[0]; + + assertThat(annotation).isInstanceOf(Description.class); - assertEquals("Test description", parameter.getDescription()); + 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 @@ -531,10 +537,14 @@ void invalidMethodWithIPrimitiveTypeDate() { private List getMethodAndExecute(String theMethodName, Class... theParamClasses) { return MethodUtil.getResourceParameters( ourFhirContext, - myMethodAndOperationParamsInnerClassesAndMethods.getDeclaredMethod(myProvider, 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");