users@jersey.java.net

Re: [Jersey] _at_QueryParam binding with Java Enum

From: Jeff Schmidt <jas_at_535consulting.com>
Date: Wed, 29 Oct 2008 14:07:40 -0600

Thanks a lot Paul. I seem to have a provider working now, and I've
learned more about the Jersey runtime in the process. :) Here's what I
came up with, using Jersey 1.0. Maybe this can help someone else. If
you have other pointers or comments on this implementation, I'd
appreciate those as well.

The goal is to resolve a string query parameter (could be a path
parameter) to a specific constant of a specific enum type. By default,
Jersey invokes the Enum.valueOf() method to do this, in which case the
parameter value must be the name of a constant. I wish to use
alternative naming to better match my XML schema. So, I modified the
resource method to accept two query parameters of different Enum
types, to make sure this solution works more generally.

     @GET
     public AnalysesList getInfoForAllAnalyses(
             @Context UriInfo uriInfo,
             @QueryParam("analysistype") AnalysisType analysisType,
             @QueryParam("analysidduplicate")
AnalysisDuplicateIdResolution analysIdDuplicate) {

I then defined a @Provider to resolve the parameter values to enum
constants. During startup, I can see that these two@ QueryParam
annotations where correctly identified by the provider:

Parameter class 'class
com.mycompany.ipaws.model.analysis.AnalysisType' is an Enum!
Parameter class 'class
com.mycompany.ipaws.model.analysis.AnalysisDuplicateIdResolution' is
an Enum!

The following is the provider itself. It uses reflection to invoke the
fromValue() method to resolve the parameter value into an enum
constant. If that method does not exist, then the standard valueOf()
method is invoked. Thus, Enums w/o the fromValue() method will still
work in the classic way. For example, AnalysisType is defined as:

@XmlEnum
@XmlType(name="AnalysisType")
public enum AnalysisType {

     @XmlEnumValue("Core")
     CORE("Core"),
     @XmlEnumValue("Tox")
     TOX("Tox"),
     @XmlEnumValue("Metabolomics")
     METABOLOMICS("Metabolomics")
     ;

     private final String value;

     AnalysisType(String v) {
         value = v;
     }

     public String value() {
         return value;
     }

     public static AnalysisType fromValue(String v) {
         for (AnalysisType c: AnalysisType.values()) {
             if (c.value.equals(v)) {
                 return c;
             }
         }
         throw new IllegalArgumentException(v.toString());
     }
}

The value field as well as the fromValue() method were both brought to
my attention by looking at classes generated from XML schema via the
JAXB xjc command.

The provider is defined as follows:

@Provider
public class QueryParamEnumInjectableProvider implements
            InjectableProvider<QueryParam, Parameter> {

     private final @Context HttpContext hc;

     public QueryParamEnumInjectableProvider(@Context HttpContext hc) {
         this.hc = hc;
     }

     public Scope getScope() {
         return Scope.PerRequest;
     }

     public Injectable<? extends Enum> getInjectable(ComponentContext
ic, QueryParam a, Parameter c) {

         if
(java.lang.Enum.class.isAssignableFrom(c.getParameterClass())) {
             System.out.println("Parameter class '" +
c.getParameterClass() + "' is an Enum!");
             return new QueryParamEnumInjectable(a.value(),
c.getParameterClass());
         }
         else
             return null;
     }

     protected static class QueryParamEnumInjectable implements
Injectable<Enum> {

         final static String METHOD_NAME_FROM_VALUE = "fromValue";
         final static Class[] METHOD_ARGS_FROM_VALUE = new Class[]
{String.class};

         final static String METHOD_NAME_VALUE_OF = "valueOf";
         final static Class[] METHOD_ARGS_VALUE_OF = new Class[]
{Class.class, String.class};

         final protected String _queryParamName;
         final protected Class _enumClass;

         QueryParamEnumInjectable(String queryParamName, Class
enumClass) {
             _queryParamName = queryParamName;
             _enumClass = enumClass;
         }

         public Enum getValue(HttpContext context) {

             final String queryParamValue =
context.getUriInfo().getQueryParameters().getFirst(_queryParamName);
             System.out.println(_queryParamName + "=" +
queryParamValue);

             /*
              * Query parameter is not mandatory.
              */
             if (queryParamValue == null)
                 return null;

             /*
              * Invoke optional fromValue method if present.
              */
             try {
                 final Method fromValueMethod =
_enumClass.getMethod(METHOD_NAME_FROM_VALUE, METHOD_ARGS_FROM_VALUE);
                 final Enum enumInstance =
(Enum)fromValueMethod.invoke(null, new Object[] { queryParamValue });
                 System.out.println(String.format("for param name: %s
of type: %s, returned enum instance: %s",
                         _queryParamName, _enumClass.getName(),
enumInstance));
                 return enumInstance;

             } catch (InvocationTargetException ex) {
                 System.out.println("Method failed with: " +
ex.getCause());
                 throw new NotFoundException();

             } catch (Exception ex) {
                 //Method does not exist or is otherwise not invocable.
             }

             /*
              * No fromValue method, so invoke standard valueOf method
              */
             try {
                 final Method valueOfMethod =
_enumClass.getMethod(METHOD_NAME_VALUE_OF, METHOD_ARGS_VALUE_OF);
                 final Enum enumInstance =
(Enum)valueOfMethod.invoke(null, new Object[] { _enumClass,
queryParamValue });
                 System.out.println(String.format("for param name: %s
of type: %s, returned enum instance: %s",
                         _queryParamName, _enumClass.getName(),
enumInstance));
                 return enumInstance;

             } catch (InvocationTargetException ex) {
                 System.out.println("Method failed with: " +
ex.getCause());
                 throw new NotFoundException();

             } catch (Exception ex) {
                 throw new ServerErrorException("Enum failure", ex);
             }
         }
     }
}

When I invoke the resource method, with Jersey LoggingFilter setup:

2 * In-bound request received
2 > GET http://localhost:8080/pa/ws/v4/rest/partner/analyses?analysistype=Core&analysidduplicate=MEDIAN
2 > host: localhost:8080
2 > user-agent: Jakarta Commons-HttpClient/3.1
2 >
analysistype=Core
for param name: analysistype of type:
com.mycompany.ipaws.model.analysis.AnalysisType, returned enum
instance: CORE
analysidduplicate=MEDIAN
for param name: analysidduplicate of type:
com.mycompany.ipaws.model.analysis.AnalysisDuplicateIdResolution,
returned enum instance: MEDIAN

Thus, the one provider was able to properly resolve the two different
enum types, one of which has the fromValue() method defined, and the
other does not, making use of the standard Enum valueOf() method.

Cheers,

Jeff
--
Jeff Schmidt
On Oct 29, 2008, at 5:32 AM, Paul Sandoz wrote:
>
> On Oct 28, 2008, at 10:50 PM, Jeff Schmidt wrote:
>
>> Hello:
>>
>> This seems like such a basic thing to want, that I'm probably just  
>> missing something there. I have a method in a resource defined as:
>>
>>    @GET
>>    public AnalysesList getInfoForAllAnalyses(
>>            @Context UriInfo uriInfo,
>>            @QueryParam("analysistype") AnalysisType analysisType) {
>>
>>        ...
>>    }
>>
>> AnalysisType is a pretty simple Enum:
>>
>> @XmlEnum
>> public enum AnalysisType {
>>
>>    @XmlEnumValue("Core")
>>    CORE("Core"),
>>    @XmlEnumValue("Tox")
>>    TOX("Tox"),
>>    @XmlEnumValue("Metabolomics")
>>    METABOLOMICS("Metabolomics")
>>    ;
>>
>>    private final String value;
>>
>>    AnalysisType(String v) {
>>        value = v;
>>    }
>>
>>    public String value() {
>>        return value;
>>    }
>>
>>    public static AnalysisType fromValue(String v) {
>>        for (AnalysisType c: AnalysisType.values()) {
>>            if (c.value.equals(v)) {
>>                return c;
>>            }
>>        }
>>        throw new IllegalArgumentException(v.toString());
>>    }
>>
>>    /*
>>    public static AnalysisType valueOf(String typeStr) {
>>        return AnalysisType.fromValue(typeStr);
>>    }*/
>> }
>>
>> Note this Enum was originally derived from an XML schema used in a  
>> SOAP API (using JAXB xjc), and it created the fromValue() method,  
>> which I assume JAXB uses to bind a String representation to an  
>> actual Enum instance. I would also like to bind a JAX-RS query  
>> parameter to this Enum using Jersey. Making a request with the  
>> query parameter defined as analysistype=CORE works great. However,  
>> I want to refer to the type as "Core" (analysistype=Core) to match  
>> the CamelCase XML standard we're using. However, this results in  
>> 404 (Not Found).
>>
>> I don't want to have to change the Enum itself and dependent code  
>> to switch from AnalysisType.CORE to AnalysisType.Core. From what I  
>> can tell, a given Java class can be bound by Jersey if it has a  
>> static valueOf(String) method. However, Enum will not let me define  
>> this method as the compiler says its already defined. I'm guessing  
>> that's how Jersey is able to bind as explained earlier; based on  
>> the actual name of the Enum elements.
>
> Yes, i think that is what is going on.
>
>
>> Also, I cannot make a constructor available to Jersey, given it's  
>> an Enum.
>>
>> Is there a good way to handle this in Jersey?  Perhaps there is  
>> some kind of provider or resolver I have to implement on my own?
>>
>
> The simplest approach is to create a wrapper class supporting the  
> static valueOf or a constructor taking a String argument.
>
> You can register an injectable for the @QueryParam and the  
> AnalysisType:
>
>    @Provider
>    public static class QueryParamInjectableProvider implements
>            InjectableProvider<QueryParam, Parameter> {
>
>        private final @Context HttpContext hc;
>
>        public QueryParamInjectableProvider(@Context HttpContext hc) {
>            this.hc = hc;
>        }
>
>        public ComponentScope getScope() {
>            return ComponentScope.PerRequest;
>        }
>
>        public Injectable<AnalysisType>  
> getInjectable(ComponentContext ic,
>                QueryParam a, Parameter c) {
>            if (AnalysisType.class != c.getParameterClass())
>                return null;
>
>            final String name = c.getSourceName();
>            return new Injectable<AnalysisType>() {
>                public AnalysisType getValue() {
>                    String value =  
> hc.getUriInfo().getQueryParameters().getFirst(name);
>
>                    return AnalysisType.fromValue(name);
>                }
>            };
>        }
>    }
>
> Note that the above does not support default values (in the absence  
> of the query parameter), but it is easy to add, and it should throw  
> a 404-based exception if the value is not recognized.
>
> I am also wondering whether it should be possible to add support for  
> any enum with a particular method like fromValue.
>
>    @Provider
>    public static class QueryParamInjectableProvider implements
>            InjectableProvider<QueryParam, Parameter> {
>
>        private final @Context HttpContext hc;
>
>        public QueryParamInjectableProvider(@Context HttpContext hc) {
>            this.hc = hc;
>        }
>
>        public ComponentScope getScope() {
>            return ComponentScope.PerRequest;
>        }
>
>        public Injectable<? extends Enum>  
> getInjectable(ComponentContext ic,
>                QueryParam a, Parameter c) {
>            if  
> (AnalysisType.class.isAssignableFrom(c.getParameterClass())
>                return null;
>
>            ....
>        }
>    }
>
>
> Hope this helps,
> Paul.
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: users-unsubscribe_at_jersey.dev.java.net
> For additional commands, e-mail: users-help_at_jersey.dev.java.net
>