Realm-java: RealmProxyClassGenerator needs to add annotation from source RealmObject class

Created on 2 Aug 2016  路  18Comments  路  Source: realm/realm-java

Goal

RealmProxyClassGenerator needs to add the annotation indicated from source RealmObject class

Expected Results

If I added an annotation from the source annotation for example:

@JsonObject
public class User extends RealmObject {

The RealmProxy class counterpart should have the annotation inherited as well:

@JsonObject
public class UserRealmProxy extends entities.realmobjects.User 
implements RealmObjectProxy, UserRealmProxyInterface 

Actual Results

No inheritance of Annotation occurring on the RealmProxy class

Version of Realm and tooling

Realm version(s): 1.1.0
Android Studio version: 2.1.2

Design-Required O-Community T-Bug

Most helpful comment

It did work. Thanks :)

But if we're saying that it's not using reflection then I suggest pursuing this issue still :)

All 18 comments

Hi @peterbetos I am not fully sure what you mean by the title for this issue. Please fill out the rest of the template as well.

Edited the issue entry. :)

Thanks :)
What is the use case for this?
Annotations are inherited so the proxy classes should count as having the annotation.

This is for RealmObjects instantiated from using Realm "where" method,
example: realm.where(User.class).findFirst();
but needs to be serialized by a parser e.g. LoganSquare.
The object returned from the where operation is usually a Proxy class

Yup, objects returned by where() is always proxy objects. Have you verified it isn't working currently because the annotations should be inherited: http://stackoverflow.com/questions/10596744/is-it-possible-for-class-to-inherit-the-annotaions-of-the-super-class

Perhaps it is LoganSquare that is not inspecting the annotations properly? I am not that familiar with LoganSquare to be able to tell how they do this.

Note, I am not inherently against us adding the annotations to the proxy class, just trying to grasp what problems it would solve and if it should be solved elsewhere instead.

It seems to be an explicit requirement for the LoganSquare parsing.

The following exception is returned if the annotation is not explicitly mentioned:
com.bluelinelabs.logansquare.NoSuchMapperException: Class io.realm.UserRealmProxy could not be mapped to a JSON object. Perhaps it hasn't been annotated with @JsonObject?

How does LoganSquare access the object data? If they use reflection it will not work as our proxy classes do not have the information in the field. So adding the annotation will not accomplish much.

A work-around that would work now is doing a copyFromRealm and then serialize that:

String json  = LoganSquare.serialize(realm.copyFromRealm(obj));

Does copyFromRealm produce the RealmObject extended version of the class?

No, it produces an instance of the original class.

@cmelchior LoganSquare uses an annotation processor to generate the JSON-parse jackson core stream code, but it uses .class to determine the type of mapper to be selected for the current given object.

    /**
     * Returns a JsonMapper for a given class that has been annotated with @JsonObject.
     *
     * @param cls The class for which the JsonMapper should be fetched.
     */
    public static <E> JsonMapper<E> mapperFor(Class<E> cls) throws NoSuchMapperException {
        JsonMapper<E> mapper = getMapper(cls);

        if (mapper == null) {
            throw new NoSuchMapperException(cls);
        } else {
            return mapper;
        }
    }

    @SuppressWarnings("unchecked")
    /*package*/ static <E> JsonMapper<E> getMapper(Class<E> cls) {
        JsonMapper<E> mapper = OBJECT_MAPPERS.get(cls);
        if (mapper == null) {
            // The only way the mapper wouldn't already be loaded into OBJECT_MAPPERS is if it was compiled separately, but let's handle it anyway
            try {
                Class<?> mapperClass = Class.forName(cls.getName() + Constants.MAPPER_CLASS_SUFFIX);
                mapper = (JsonMapper<E>)mapperClass.newInstance();
                OBJECT_MAPPERS.put(cls, mapper);
            } catch (Exception ignored) { }
        }
        return mapper;
    }

As such, mapperFor(Class<?> clazz) will fail for the realm proxy. It works with copyFromRealm() though

It did work. Thanks :)

But if we're saying that it's not using reflection then I suggest pursuing this issue still :)

Thanks @Zhuinden, although the crux of the problem is what the mapper class is doing. Most JSON libraries uses reflection, which doesn't work on Realm proxy classes. In that case the only work-around is using copyFromRealm() first. Us adding the annotation to the proxy class will not fix it.

@cmelchior I don't think the Mapper class would itself fail. It uses annotation processing to generate it.

This is an example

import com.bluelinelabs.logansquare.JsonMapper;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.IOException;
import java.lang.Long;
import java.lang.Override;
import java.lang.String;
import java.lang.SuppressWarnings;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unsafe,unchecked")
public final class FeedItemBO$$JsonObjectMapper extends JsonMapper<FeedItemBO> {
  @Override
  public FeedItemBO parse(JsonParser jsonParser) throws IOException {
    FeedItemBO instance = new FeedItemBO();
    if (jsonParser.getCurrentToken() == null) {
      jsonParser.nextToken();
    }
    if (jsonParser.getCurrentToken() != JsonToken.START_OBJECT) {
      jsonParser.skipChildren();
      return null;
    }
    while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
      String fieldName = jsonParser.getCurrentName();
      jsonParser.nextToken();
      parseField(instance, fieldName, jsonParser);
      jsonParser.skipChildren();
    }
    return instance;
  }

  @Override
  public void parseField(FeedItemBO instance, String fieldName, JsonParser jsonParser) throws IOException {
    if ("category_ids".equals(fieldName)) {
      if (jsonParser.getCurrentToken() == JsonToken.START_ARRAY) {
        ArrayList<Long> collection1 = new ArrayList<Long>();
        while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
          Long value1;
          value1 = jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong());
          collection1.add(value1);
        }
        instance.setCategoryIds(collection1);
      } else {
        instance.setCategoryIds(null);
      }
    } else if ("created_time".equals(fieldName)) {
      instance.setCreatedTime(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
    } else if ("id".equals(fieldName)) {
      instance.setId(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
    } else if ("image_url".equals(fieldName)) {
      instance.setImageUrl(jsonParser.getValueAsString(null));
    } else if ("is_favorited".equals(fieldName)) {
      instance.setIsFavorited(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
    } else if ("is_important".equals(fieldName)) {
      instance.setIsImportant(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
    } else if ("is_liked".equals(fieldName)) {
      instance.setIsLiked(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
    } else if ("number_of_likes".equals(fieldName)) {
      instance.setNumberOfLikes(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Integer.valueOf(jsonParser.getValueAsInt()));
    } else if ("result_type_code".equals(fieldName)) {
      instance.setResultTypeCode(jsonParser.getValueAsString(null));
    } else if ("short_description".equals(fieldName)) {
      instance.setShortDescription(jsonParser.getValueAsString(null));
    } else if ("title".equals(fieldName)) {
      instance.setTitle(jsonParser.getValueAsString(null));
    } else if ("type_code".equals(fieldName)) {
      instance.setTypeCode(jsonParser.getValueAsString(null));
    } else if ("updated_time".equals(fieldName)) {
      instance.setUpdatedTime(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
    }
  }

  @Override
  public void serialize(FeedItemBO object, JsonGenerator jsonGenerator, boolean writeStartAndEnd) throws IOException {
    if (writeStartAndEnd) {
      jsonGenerator.writeStartObject();
    }
    final List<Long> lslocalcategory_ids = object.getCategoryIds();
    if (lslocalcategory_ids != null) {
      jsonGenerator.writeFieldName("category_ids");
      jsonGenerator.writeStartArray();
      for (Long element1 : lslocalcategory_ids) {
        if (element1 != null) {
          jsonGenerator.writeNumber(element1);
        }
      }
      jsonGenerator.writeEndArray();
    }
    if (object.getCreatedTime() != null) {
      jsonGenerator.writeNumberField("created_time", object.getCreatedTime());
    }
    if (object.getId() != null) {
      jsonGenerator.writeNumberField("id", object.getId());
    }
    if (object.getImageUrl() != null) {
      jsonGenerator.writeStringField("image_url", object.getImageUrl());
    }
    if (object.getIsFavorited() != null) {
      jsonGenerator.writeBooleanField("is_favorited", object.getIsFavorited());
    }
    if (object.getIsImportant() != null) {
      jsonGenerator.writeBooleanField("is_important", object.getIsImportant());
    }
    if (object.getIsLiked() != null) {
      jsonGenerator.writeBooleanField("is_liked", object.getIsLiked());
    }
    if (object.getNumberOfLikes() != null) {
      jsonGenerator.writeNumberField("number_of_likes", object.getNumberOfLikes());
    }
    if (object.getResultTypeCode() != null) {
      jsonGenerator.writeStringField("result_type_code", object.getResultTypeCode());
    }
    if (object.getShortDescription() != null) {
      jsonGenerator.writeStringField("short_description", object.getShortDescription());
    }
    if (object.getTitle() != null) {
      jsonGenerator.writeStringField("title", object.getTitle());
    }
    if (object.getTypeCode() != null) {
      jsonGenerator.writeStringField("type_code", object.getTypeCode());
    }
    if (object.getUpdatedTime() != null) {
      jsonGenerator.writeNumberField("updated_time", object.getUpdatedTime());
    }
    if (writeStartAndEnd) {
      jsonGenerator.writeEndObject();
    }
  }
}

But I use separate RealmObjects and JsonObjects.

I checked the JSONMapper class
https://raw.githubusercontent.com/bluelinelabs/LoganSquare/6c5ec5281fb58d85a99413b7b6f55e9ef18a6e06/core/src/main/java/com/bluelinelabs/logansquare/JsonMapper.java

And it seems to just use the JSONParer from Jackson:
https://github.com/FasterXML/jackson-core/blob/master/src/main/java/com/fasterxml/jackson/core/JsonParser.java

Not sure if Jackson's JSONParser is using reflection though?

@peterbetos I'm pretty sure the streaming API that LoganSquare uses does not use reflection during the mapping process. That's exactly why it's fast.

@cmelchior just to prove that that's the part that fails,

/**
 * The exception that will be thrown in the event that LoganSquare.mapperFor() is
 * called with a class that hasn't been declared as a @JsonObject.
 */
public class NoSuchMapperException extends RuntimeException {

    public NoSuchMapperException(Class cls) {
        super("Class " + cls.getCanonicalName() + " could not be mapped to a JSON object. Perhaps it hasn't been annotated with @JsonObject?");
    }
}

The guy above received exactly this message, which means getMapperFor(Class<?>) failed to return a mapper.

Although it's weird because if the proxy class is inherited, then why isn't it auto-generated on second try...?

It probably goes into } catch(Exception e) { /* ignored */ } which isn't very helpful.

I believe somehow you have to put the mappers for the normal object into the LoganSquare mapper map for the .class of the proxies... >.>

getMapperFor() is a static method and the OBJECT_MAPPERS map is private static final, so you'd probably need to use reflection for that and do it manually... -_-

The other alternative is a new TypeConverter for each proxy, but that's even hackier.

Moving annotations to the proxy class might have some unforeseen consequences (for other annotation processors). We will have to look a bit more into it in order to understand the issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AAChartModel picture AAChartModel  路  3Comments

aschrijver picture aschrijver  路  3Comments

bryanspano picture bryanspano  路  3Comments

nolanamy picture nolanamy  路  3Comments

jjorian picture jjorian  路  3Comments