joninx
joninx

Reputation: 1780

How to return objects in Jersey 2.x

I've got a web service which manages Parada objects. What I want to achieve seems straightforward: return lists of these objects:

List<Parada> list

This list is returned using a Service class which uses another DAO class, just commenting it out.

Besides, my common practise is that every web method return a Response using ResponseBuilder, as in here:

return Response.ok(obj, MediaType.APPLICATION_JSON).build();

This is an example of one of my web methods:

@GET
@Consumes(value = MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
@Path("{idParadaGtfs}")
public Response getParadasPorIdGtfs(
    @PathParam(value = "idParadaGtfs") Integer pCodigoParadaEnGtfs
){
    try{
        getServiceIfNecessary();
        List<Parada> paradas = service.getParadas(pCodigoParadaEnGtfs);
        return Response.ok(paradas, MediaType.APPLICATION_JSON).build();
    }catch(HibernateException e){
        String msg = "Error HibernateException: " + e.getMessage();
        LogHelper.logError(logger, msg, true);
        e.printStackTrace();
        return Response.serverError().tag(msg).build();
    }catch(Exception e){
        String msg = "Error Exception: " + e.getMessage();
        LogHelper.logError(logger, msg, true);
        e.printStackTrace();
        return Response.serverError().tag(msg).build();
    }

}

Unfortunately, I'm not receiving any object and I get the following error everytime I execute the web method described above:

nov 26, 2015 2:20:16 PM org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor aroundWriteTo
GRAVE: MessageBodyWriter not found for media type=application/json, type=class java.util.ArrayList, genericType=java.util.List<model.Parada>.

What do I have to implement to let my web methods build Responses using Lists?

Thank you!

EDIT:

I've been able to make it work by making some changes and additions, which I'll describe now.

First of all, I've added a Parada container class, ParadaContainer:

    import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlRootElement;

import com.ingartek.ws.paradasasociadasws.model.Parada;

@XmlRootElement
public class ParadaContainer implements Serializable {

    private static final long serialVersionUID = 6535386309072039406L;
    private List<Parada> paradas;

    public ParadaContainer(ArrayList<Parada> pParadas) {
        this.setParadas(pParadas);
    }

    public List<Parada> getParadas() {
        return paradas;
    }

    public void setParadas(List<Parada> paradas) {
        this.paradas = paradas;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("ParadaContainer [");
        if (paradas != null) {
            builder.append("paradas=");
            for(Parada p : paradas){
                builder.append(p.toString());
            }

        }
        builder.append("]");
        return builder.toString();
    }

}

Now, I'm not returning a List of Parada objects, instead I return a single ParadaContainer object:

ParadaContainer paradas = new ParadaContainer(new ArrayList<Parada>(service.getParadas()));

return Response
        .ok(paradas)
        .type(MediaType.APPLICATION_JSON)
        .build();

I don't know whether they are mandatory or not, but I've created another class (MyObjectMapperProvider)...

import javax.ws.rs.ext.ContextResolver;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {

    final ObjectMapper defaultObjectMapper;

    public MyObjectMapperProvider() {
        defaultObjectMapper = createDefaultMapper();
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
            return defaultObjectMapper;
    }

    private static ObjectMapper createDefaultMapper() {
        final ObjectMapper result = new ObjectMapper();
        result.configure(SerializationFeature.INDENT_OUTPUT, true);

        return result;
    }
}

...and edited my Application class and added some lines (see as of *Jackson * comment until Clases de Servicios comment):

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.core.Application;

import org.glassfish.jersey.jackson.JacksonFeature;

import com.ingartek.ws.paradasasociadasws.ws.ParadasWS;

public class App extends Application {

    private final Set<Class<?>> classes;

    public App() {
        HashSet<Class<?>> c = new HashSet<Class<?>>();
        // Filtro CORS:
        c.add(CORSFilter.class);

        // Jackson
        c.add(MyObjectMapperProvider.class);
        c.add(JacksonFeature.class);

        // Clases de Servicios:
        c.add(ParadasWS.class);
        classes = Collections.unmodifiableSet(c);
    }

    @Override
    public Set<Class<?>> getClasses() {
        return classes;
    }

}

Afterwards, I've edited my class model by adding some annotations to them (@XmlRootElement and @JsonProperty; removed irrelevant getters, setters, hashCode, equals and toString methods):

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;

import com.fasterxml.jackson.annotation.JsonProperty;

@XmlRootElement(name = "grupo")
@Entity
@Table(name = "grupos_cercania_exacta")
public class Grupo implements Serializable {

    @Transient
    private static final long serialVersionUID = -5679016396196675191L;

    @JsonProperty("id")
    @Id
    @Column(name = "id_grupo")
    private Integer id;

    ...

}

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;

import com.fasterxml.jackson.annotation.JsonProperty;

@XmlRootElement(name = "operador")
@Entity
@Table(name = "operadores_asociados")
public class Operador implements Serializable {

    @Transient
    private static final long serialVersionUID = -7557099187432476588L;

    /*
        Atributos
     */
    @JsonProperty("codigo")
    @Id
    @Column(name = "codigo_operador", insertable = false, updatable = false)
    private Integer codigo;
    @JsonProperty("nombre")
    @Column(name = "descripcion_corta", insertable = false, updatable = false)
    private String nombre;
    @JsonProperty("descripcion")
    @Column(name = "descripcion_larga", insertable = false, updatable = false)
    private String descripcion;
    @JsonProperty("web")
    @Column(name = "direccion_web", insertable = false, updatable = false)
    private String web;
    @JsonProperty("telefono")
    @Column(name = "telefono", insertable = false, updatable = false)
    private String telefono;

    ...

}

import java.io.Serializable;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;

import com.fasterxml.jackson.annotation.JsonProperty;

@XmlRootElement(name = "parada")
@Entity
@Table(name = "paradas_asociadas")
public class Parada implements Serializable {

    @Transient
    private static final long serialVersionUID = -3594254497389126197L;

    @JsonProperty("id")
    @Id
    @Column(name = "id")
    private UUID id;
    @JsonProperty("codigoMunicipio")
    @Column(name = "codigo_municipio")
    private Integer codigoMunicipio;
    @JsonProperty("nombre")
    @Column(name = "nombre")
    private String nombre;
    @JsonProperty("descripcion")
    @Column(name = "descripcion")
    private String descripcion;
    @JsonProperty("idGtfs")
    @Column(name = "id_gtfs")
    private Integer idGtfs;
    @JsonProperty("idWs")
    @Column(name = "id_ws")
    private Integer idWs;
    @JsonProperty("latitud")
    @Column(name = "latitud")
    private Double latitud;
    @JsonProperty("longitud")
    @Column(name = "longitud")
    private Double longitud;
    @JsonProperty("utmX")
    @Column(name = "utm_x")
    private Double utmX;
    @JsonProperty("utmY")
    @Column(name = "utm_y")
    private Double utmY;
    @JsonProperty("grupo")
    @ManyToOne
    @JoinColumn(name = "grupo_cercania_exacta_id")
    private Grupo grupo;
    @JsonProperty("operador")
    @ManyToOne
    @JoinColumn(name = "operador")
    private Operador operador;

    ...

}

I've to admit that I've had some problems just after these changes. Sharp people could've realised that there is a missing attribute regarding the previous Parada class: the lack of Point attribute. This attribute was causing me some problems, this is, the absence of a Serializer and a Serializer was preventing me from creating a successful JSON. So I googled it out and found three options:

  1. Remove the Point item. This was my ultimate choice, as Point was superfluous due to the existence of Latitude and Longitude elements and because it just could bother or confuse the final user.
  2. Creating a custom Serializer and Deserializer. Fortunately I found the following link, which describes the process of creating them. The following is described in here:

Add these annotations to our coordinates field:

@JsonSerialize(using = PointToJsonSerializer.class)
@JsonDeserialize(using = JsonToPointDeserializer.class)

Create such serializer:

import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.vividsolutions.jts.geom.Point;

public class PointToJsonSerializer extends JsonSerializer<Point> {

    @Override
    public void serialize(Point value, JsonGenerator jgen,
            SerializerProvider provider) throws IOException,
            JsonProcessingException {

        String jsonValue = "null";
        try
        {
            if(value != null) {             
                double lat = value.getY();
                double lon = value.getX();
                jsonValue = String.format("POINT (%s %s)", lat, lon);
            }
        }
        catch(Exception e) {}

        jgen.writeString(jsonValue);
    }

}

Create such deserializer:

import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.PrecisionModel;

public class JsonToPointDeserializer extends JsonDeserializer<Point> {

    private final static GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 26910); 

    @Override
    public Point deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        try {
            String text = jp.getText();
            if(text == null || text.length() <= 0)
                return null;

            String[] coordinates = text.replaceFirst("POINT ?\\(", "").replaceFirst("\\)", "").split(" ");
            double lat = Double.parseDouble(coordinates[0]);
            double lon = Double.parseDouble(coordinates[1]);

            Point point = geometryFactory.createPoint(new Coordinate(lat, lon));
            return point;
        }
        catch(Exception e){
            return null;
        }
    }

}
  1. The last option is to use Jackson Datatype JTS library, whose github repository lays here.

I lasted some hours so that I could find these solutions, but finally I got them. Hope it helps to someone. Thank you!

Upvotes: 1

Views: 5649

Answers (2)

Paul Samsotha
Paul Samsotha

Reputation: 209002

Either you don't have a JSON provider (I am guessing you do) or you are using MOXy. Under the latter assumption, with MOXy, it needs to know the type information in order to be able to serialize. When you return Response, you are wrapping the object, which takes away type information (because of type erasure), as opposed to if you were doing

@GET
public List<Parada> get() {}

Here the type information is known. But doing

@GET
public Response get() {
    List<Parada> list..
     return Response.ok(list)...
}

The type is hidden and erased by the time the entity reaches the serialization phase of the processing.

To get around this, GenericEntity was introduced

Normally type erasure removes generic type information such that a Response instance that contains, e.g., an entity of type List<String> appears to contain a raw List<?> at runtime. When the generic type is required to select a suitable MessageBodyWriter, this class may be used to wrap the entity and capture its generic type.

So you can do

List<Parada> paradas = ...
GenericEntity<List<Parada>> entity = new GenericEntity<List<Parada>>(paradas){};
return Response.ok(entity, ...)...

Second option, is to instead of using MOXy, use Jackson instead. With Jackson, the type info is not needed (in most cases), as the serializer just introspects and the bean bean properties to get the data.

Upvotes: 4

Albert Bos
Albert Bos

Reputation: 2062

It is not allowed to send a List back. Probably because List has no @XmlRootElement notation. You can create your own container:

@XmlRootElement
public class ParadaContainer implements Serializable {
    private List<Parada> list;

    public List<Parada> getList() {
        return list;
    }

    public void setList(List<Parada> list) {
        this.list = list;
    }
}

You part will look like:

try{
        getServiceIfNecessary();
        List<Parada> paradas = service.getParadas(pCodigoParadaEnGtfs);
        ParadaContainer paradaContainer = new ParadaContainer();
        paradaContainer.setList(paradas);
        return Response.ok(paradaContainer, MediaType.APPLICATION_JSON).build();
    }

Upvotes: 1

Related Questions