eclipse-ee4j / jersey

Eclipse Jersey Project - Read our Wiki:
https://github.com/eclipse-ee4j/jersey/wiki
Other
692 stars 354 forks source link

EntityFilteringFeature disables jackson polymorphism #3658

Open jerseyrobot opened 7 years ago

jerseyrobot commented 7 years ago

I'm experimenting with upgrading my ReST service from 2.25.1 to to 2.26-b09 in order to use Jersey's new EntityFilteringFeature with Jackson JSON. When I do NOT register the EntityFilteringFeature in the ResourceConfig OR when I do NOT register JacksonFeature, I get this output from my ReST resource:

{"innerData":{"item1":"item1","item2":"item2"},"name":"Got it!"}

When I DO register EntityFilteringFeature AND JacksonFeature, I get this output:

{"name":"Got it!","innerData":{}}

Note the contents of the 'innerData' field are missing when EntityFilteringFeature AND JacksonFeature are registered. I think perhaps this is because the innerData object is of type InnerDataBase - a base class with no fields, and the fields of the subclass that's actually allocated are not being rendered - only the empty base class is being rendered.

It seems like Jersey's EntityFilteringFeature is disabling or breaking proper Jackson handling of polymorphic types. Note there are no generic type issues here - only a base class and a subclass with two fields.

Note also that this works properly when EntityFilteringFeature is registered, but JacksonFeature is NOT registered - which tells me that the default Moxy json processor is working fine.

Code:

Main.java:

public class Main {
    public static final String BASE_URI = "http://0.0.0.0:8080/myapp/";

    public static HttpServer startServer() {
        // create a resource config that scans for JAX-RS resources and providers
        // in com.example package
        final ResourceConfig rc = new ResourceConfig()
                .packages("com.example")
                .register(EntityFilteringFeature.class)
                .register(JacksonFeature.class)
        ;

        // create and start a new instance of grizzly http server
        // exposing the Jersey application at BASE_URI
        return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
    }

    public static void main(String[] args) throws IOException {
        final HttpServer server = startServer();
        System.out.println(String.format("Jersey app started with WADL available at "
                + "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
        System.in.read();
        server.shutdown();
    }
}

MyResource.java:

@Path("myresource")
@Produces(MediaType.APPLICATION_JSON)
public class MyResource {

    public static class InnerDataBase {
    }

    public static class InnerData extends InnerDataBase {
        public String item1 = "item1";
        public String item2 = "item2";
    }

    public static class Data {
        public String name = "Got it!";
        public InnerDataBase innerData = new InnerData();     // NOTE: innerData is of base type
    }

    @GET
    public Data getIt() {
        return new Data();
    }
}

Thanks in advance, John

P.S. I generated the original project from the jersey-quickstart-grizzly2 maven archetype and then added dependencies from https://github.com/jersey/jersey/tree/master/examples/entity-filtering.

jerseyrobot commented 6 years ago
jerseyrobot commented 7 years ago

@jcalcote Commented I've dived in deeper to see if I could figure out the problem. I've analyzed two areas of the code:

  1. In JacksonObjectProvider::transform, the ObjectGraph is transformed into a Jackson FilterProvider which is later used by the object mapper to determine which fields to filter out of the entity object and its sub-entities. The problem I've found is that this JacksonObjectProvider::transform method only works on static type information (that's all it has access to at the time transform is executed - no objects are yet available as the FilterProvider (jersey - FilteringFilterProvider) is attached to the object mapper before any data is serialized. Hence, the configured Jackson filter only knows about static typing information - it can't know about the runtime types of the entities.

  2. At run time when the filter is execute, the POJO is actually available. Additional work needs to be done at this time to determine the run time type of the entity being serialized (as opposed to the static type of the field). I tried to enhance the filtering algorithm to look for more than just statically typed fields that were allowed to pass the filter by adding additional checks to JacksonObjectProvider::FilteringPropertyFilter::include such that if the fieldName was not found in fields and subfilters, then it would also check to see if the run time type of the POJO assigned to the field was a subclass (or in the subclass hierarchy) of the Filter's entityClass (field).

The assumption here is that if the filter root field is allowed to pass, then all subfields in the object hierarchy below this point are also allowed to pass. This is the code I added:

private boolean isSubType(Object pojo) {
    if (pojo != null) {
        Class<?> clazz = pojo.getClass().getSuperclass();
        while (clazz != null) {
            if (clazz == entityClass) {
                return true;
            }
            clazz = clazz.getSuperclass();
        }
    }
    return false;
}

private boolean include(final String fieldName) {
    return include(fieldName, null);
}

private boolean include(final String fieldName, final Object pojo) {
    return fields.contains(fieldName) || subfilters.containsKey(fieldName) || isSubType(pojo);
}

@Override
public void serializeAsField(final Object pojo, final JsonGenerator jgen, final SerializerProvider prov, final PropertyWriter writer) throws Exception {
    if (include(writer.getName(), pojo) {
        writer.serializeAsFiled(pojo, jgen, prov);
    }
}

// same change was made to serializeAsElement

The only issue I can see with using this "fix" is that it still doesn't allow filtering annotations to be applied to polymorphic sub-types of the static class hierarchy. Since the ObjectGraph builder is not aware of polymorphic subtypes, it can't include any filtering annotations in the graph used to build the filter.

I'm working on another version that uses the Reflections library to pull all possible subclasses of model classes from the classpath and add them to the entity graph. I have that part working, but am still trying to figure out how to use this extended data during serialization to ensure that the subclasses actually get serialized if the parent field was annotated properly.

John