Using Joda DateTime as a Jersey parameter?
Asked Answered
L

5

12

I'd like to use Joda's DateTime for query parameters in Jersey, but this isn't supported by Jersey out-of-the-box. I'm assuming that implementing an InjectableProvider is the proper way to add DateTime support.

Can someone point me to a good implementation of an InjectableProvider for DateTime? Or is there an alternative approach worth recommending? (I'm aware I can convert from Date or String in my code, but this seems like a lesser solution).

Thanks.

Solution:

I modified Gili's answer below to use the @Context injection mechanism in JAX-RS rather than Guice.

Update: This may not work properly if UriInfo isn't injected in your service method parameters.

import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;
import java.util.List;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import org.joda.time.DateTime;

/**
 * Enables DateTime to be used as a QueryParam.
 * <p/>
 * @author Gili Tzabari
 */
@Provider
public class DateTimeInjector extends PerRequestTypeInjectableProvider<QueryParam, DateTime>
{
    private final UriInfo uriInfo;

    /**
     * Creates a new DateTimeInjector.
     * <p/>
     * @param uriInfo an instance of {@link UriInfo}
     */
    public DateTimeInjector( @Context UriInfo uriInfo)
    {
        super(DateTime.class);
        this.uriInfo = uriInfo;
    }

    @Override
    public Injectable<DateTime> getInjectable(final ComponentContext cc, final QueryParam a)
    {
        return new Injectable<DateTime>()
        {
            @Override
            public DateTime getValue()
            {
                final List<String> values = uriInfo.getQueryParameters().get(a.value());

                if( values == null || values.isEmpty())
                    return null;
                if (values.size() > 1)
                {
                    throw new WebApplicationException(Response.status(Status.BAD_REQUEST).
                        entity(a.value() + " may only contain a single value").build());
                }
                return new DateTime(values.get(0));
            }
        };
    }
}
Lark answered 20/11, 2012 at 21:56 Comment(0)
K
5

Here is an implementation that depends on Guice. You can using a different injector with minor modifications:

import com.google.inject.Inject;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;
import java.util.List;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import org.joda.time.DateTime;

/**
 * Enables DateTime to be used as a QueryParam.
 * <p/>
 * @author Gili Tzabari
 */
@Provider
public class DateTimeInjector extends PerRequestTypeInjectableProvider<QueryParam, DateTime>
{
    private final com.google.inject.Provider<UriInfo> uriInfo;

    /**
     * Creates a new DateTimeInjector.
     * <p/>
     * @param uriInfo an instance of {@link UriInfo}
     */
    @Inject
    public DateTimeInjector(com.google.inject.Provider<UriInfo> uriInfo)
    {
        super(DateTime.class);
        this.uriInfo = uriInfo;
    }

    @Override
    public Injectable<DateTime> getInjectable(final ComponentContext cc, final QueryParam a)
    {
        return new Injectable<DateTime>()
        {
            @Override
            public DateTime getValue()
            {
                final List<String> values = uriInfo.get().getQueryParameters().get(a.value());
                if (values.size() > 1)
                {
                    throw new WebApplicationException(Response.status(Status.BAD_REQUEST).
                        entity(a.value() + " may only contain a single value").build());
                }
                if (values.isEmpty())
                    return null;
                return new DateTime(values.get(0));
            }
        };
    }
}

There are no Guice bindings. @Provider is a JAX-RS annotation. Guice just needs to be able to inject UriInfo and Jersey-Guice provides that binding.

Kidwell answered 21/11, 2012 at 19:23 Comment(2)
Thanks, Gili. I'm not using Guice, but I wonder if UriInfo can be injected with JAX-RS's @Context injection?Lark
Yep, @Context works for injecting UriInfo. I also modified your code slightly to handle the case of null DateTime query parameters. My adaptation is posted as an edit to my question. Thanks!!Lark
B
2

Another option to deal with the sending of Joda DateTime objects between the client-server is to marshal/de-marshal them explicitly using an adapter and an according annotation. The principle is to marshal it as Long object while de-marshalling instantiates a new DateTime object using the Long object for the constructor call. The Long object is obtained via the getMillis method. To have this work, specify the adapter to use in the classes that have a DateTime object:

@XmlElement(name="capture_date")
@XmlJavaTypeAdapter(XmlDateTimeAdapter.class)
public DateTime getCaptureDate() { return this.capture_date; }
public void setCaptureDate(DateTime capture_date) { this.capture_date = capture_date; }

Then write the adapter and the XML class to encapsulate the Long object:

import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

/**
 * Convert between joda datetime and XML-serialisable millis represented as long
 */
public class XmlDateTimeAdapter  extends XmlAdapter<XmlDateTime, DateTime> {

    @Override
    public XmlDateTime marshal(DateTime v) throws Exception {

        if(v != null)
            return new XmlDateTime(v.getMillis());
        else
            return new XmlDateTime(0); 


    }

    @Override
    public DateTime unmarshal(XmlDateTime v) throws Exception {

        return new DateTime(v.millis, DateTimeZone.UTC);
    }
}


import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * XML-serialisable wrapper for joda datetime values.
 */
@XmlRootElement(name="joda_datetime")
public class XmlDateTime {

    @XmlElement(name="millis") public long millis;

    public XmlDateTime() {};

    public XmlDateTime(long millis) { this.millis = millis; }   

}

If all goes to plan, the DateTime objects should be marshalled/de-marshalled by using the adaptor; check this by setting breakpoints in the adapter.

Bertrand answered 5/12, 2014 at 2:10 Comment(0)
F
1

From reading the documentation, it would appear that you'll need to have your method return a String, which you then turn into a DateTime, I suppose using the DateTime(long) constructor, there's a (relatively) easy-to-follow example at codehale, let me know if you'd like me to have a go at it.

Freddiefreddy answered 20/11, 2012 at 22:22 Comment(0)
A
1

@ Gili, sorry I don't have the required reputation to directly comment your post, but could you please:

  • add the import statements used for your implementation?
  • add an example of how you bind everything with Guice?

Thank you very much in advance.

M.


PROBLEMS:

I would be interested in doing the same as HolySamosa, and I use Guice as well, but I face the below issues.

If I add:

bind(DateTimeInjector.class);

in my GuiceServletContextListener, I get:

java.lang.RuntimeException: 
The scope of the component class com.foo.mapping.DateTimeInjector must be a singleton

and if I add @Singleton on the DateTimeInjector class, I get:

GRAVE: The following errors and warnings have been detected with resource and/or provider classes:
SEVERE: Missing dependency for method public java.util.List com.foo.ThingService.getThingByIdAndDate(java.lang.String,org.joda.time.DateTime) at parameter at index 1
SEVERE: Method, public java.util.List com.foo.ThingService.getThingByIdAndDate(java.lang.String,org.joda.time.DateTime), annotated with GET of resource, class com.foo.ThingService, is not recognized as valid resource method.

ADVICES / SOLUTIONS:

  • Pay attention to what annotation you use (unlike me)! For example I was actually using @PathParam instead of @QueryParam.
  • In your service, you do not need to have UriInfo uriInfo in the method's signature. Just the functional parameters should be enough and it should work whether UriInfo is present or not.
  • Guice needed to be configured with the below to be able to pick up the injector.

Example:

// Configure Jersey with Guice:
Map<String, String> settings = new HashMap<String, String>();
settings.put(PackagesResourceConfig.PROPERTY_PACKAGES, "com.foo.mapping");
serve("/*").with(GuiceContainer.class, settings);

FULL SOLUTION:

import java.util.List;

import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;

import org.joda.time.DateTime;

import com.google.inject.Inject;
import com.foo.utils.DateTimeAdapter;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;

/**
 * Enables DateTime to be used as a PathParam.
 */
@Provider
public class DateTimeInjector extends PerRequestTypeInjectableProvider<PathParam, DateTime> {
    private final com.google.inject.Provider<UriInfo> uriInfo;

    /**
     * Creates a new DateTimeInjector.
     * 
     * @param uriInfo
     *            an instance of {@link UriInfo}
     */
    @Inject
    public DateTimeInjector(com.google.inject.Provider<UriInfo> uriInfo) {
        super(DateTime.class);
        this.uriInfo = uriInfo;
    }

    @Override
    public Injectable<DateTime> getInjectable(final ComponentContext context, final PathParam annotation) {
        return new Injectable<DateTime>() {
            @Override
            public DateTime getValue() {
                final List<String> values = uriInfo.get().getPathParameters().get(annotation.value());

                if (values == null) {
                    throwInternalServerError(annotation);
                }

                if (values.size() > 1) {
                    throwBadRequestTooManyValues(annotation);
                }

                if (values.isEmpty()) {
                    throwBadRequestMissingValue(annotation);
                }

                return parseDate(annotation, values);
            }

            private void throwInternalServerError(final PathParam annotation) {
                String errorMessage = String.format("Failed to extract parameter [%s] using [%s]. This is likely to be an implementation error.",
                        annotation.value(), annotation.annotationType().getName());
                throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(errorMessage).build());
            }

            private void throwBadRequestTooManyValues(final PathParam annotation) {
                String errorMessage = String.format("Parameter [%s] must only contain one single value.", annotation.value());
                throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(errorMessage).build());
            }

            private void throwBadRequestMissingValue(final PathParam annotation) {
                String errorMessage = String.format("Parameter [%s] must be provided.", annotation.value());
                throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(errorMessage).build());
            }

            private DateTime parseDate(final PathParam annotation, final List<String> values) {
                try {
                    return DateTimeAdapter.parse(values.get(0));
                } catch (Exception e) {
                    String errorMessage = String.format("Parameter [%s] is formatted incorrectly: %s", annotation.value(), e.getMessage());
                    throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(errorMessage).build());
                }
            }

        };
    }
}
Aftonag answered 22/11, 2012 at 8:12 Comment(2)
@HolySamosa: using @Context without Guice doesn't seem to work if you don't provide UriInfo uriInfo in your service's method parameters. Do you only have DateTime parameters, or some other Jersey specific parameters too? e.g. public void saveResource(@Context UriInfo uriInfo, @QueryParam("date") DateTime date) {...} Or do you use some other "trick"?Homoeroticism
@Marc: As it happens, I was already providing UriInfo in my services method parameters so I didn't realize this is an issue. Admittedly, I haven't really wrapped my head around the Provider classes yet. I would think there is a way to accomplish this without needing to inject UriInfo, but I could be wrong...Lark
K
0

Another alternative is to write a simple wrapper class with a String argument constructor. Such a class then doesn't need a custom injector registered with Jersey to be injected as a query parameter.

public class DateTimeParam {
    private final DateTime value;

    public DateTimeParam(String string) {
        this.value = DateTime.parse(string);
    }

    public DateTime get() {
        return value;
    }
}

Even though this requires converting from the wrapper to DateTime in your code, this is better than converting from String to DateTime in your code, as you don't have to handle failure to parse - it will be handled by Jersey in the same way as any other type.

Kirovograd answered 6/6, 2023 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.