[JAVA-3869] @BsonId and @BsonProperty are disregarded for 'get_id()' accessor Created: 21/Oct/20  Updated: 28/Oct/20  Resolved: 28/Oct/20

Status: Closed
Project: Java Driver
Component/s: Codecs
Affects Version/s: 4.1.1
Fix Version/s: None

Type: New Feature Priority: Major - P3
Reporter: Xtra Coder Assignee: Jeffrey Yemin
Resolution: Won't Fix Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

I'm evaluating steps required to migrate some legacy code to using of current mongodb java driver with embedded POJO mappgin and have problems with POJOs having '_id' field exposed via 'get_id()' accessor.

I tried adding @BsonId annotation with no help. After digging through sources I've found the problem - @BsonId and @BsonProperty annotations are not used properly during introspection.

The problem is here: https://github.com/mongodb/mongo-java-driver/blob/r4.1.1/bson/src/main/org/bson/codecs/pojo/PropertyReflectionUtils.java#L33

Code excludes all methods that do not follow getters/setters naming conventions for java beans before annotations processing comes into place - this way user has no chance to override non-standard cases via @BsonId and @BsonProperty annotations.

Here is the portion of code I used locally to fix the problem. Please apply if it seems appropriate:

final class PropertyReflectionUtils {
    private PropertyReflectionUtils() {}
 
    private static final String IS_PREFIX = "is";
    private static final String GET_PREFIX = "get";
    private static final String SET_PREFIX = "set";
 
    static boolean isGetter(final Method method) {
        if (method.getParameterCount() > 0)
            return false;
 
        if (isPropertyAccessor(method, GET_PREFIX) || isPropertyAccessor(method, SET_PREFIX))
            return true;
 
        return false;
    }
 
    static boolean isSetter(final Method method) {
        if (method.getParameterCount() != 1)
            return false;
 
        if (isPropertyAccessor(method, SET_PREFIX))
            return true;
 
        return false;
    }
 
    static boolean isPropertyAccessor(Method method, String prefix) {
        String name = method.getName();
        return name.length() > prefix.length()
            && name.startsWith(prefix)
            && (Character.isUpperCase(name.charAt(prefix.length()))
                || method.isAnnotationPresent(BsonId.class)
                || method.isAnnotationPresent(BsonProperty.class));
    }

 



 Comments   
Comment by Jeffrey Yemin [ 28/Oct/20 ]

That's what I figured you meant but I just wanted to confirm.

I'm going to close this issue for now but if we get more requests for this feature it can be re-opened.

Comment by Xtra Coder [ 28/Oct/20 ]

More quoting from myself: "... I have to create own implementation of CodecRegistry ..." to handle my another requirement (see stackoverflow) missing in mongodb codec and while implementing that CodecRegistry I'll also do for myself the staff mentioned in this issue. So I guess there is no need to implement this improvement if it has sense just for me .

 

Comment by Jeffrey Yemin [ 28/Oct/20 ]

Hi xtracoder@gmail.com

Sorry, I'm not sure I understand what you mean by:

This means handling of underscored properties will be implemented 'by the way'.

Can you elaborate?

Comment by Xtra Coder [ 26/Oct/20 ]

Jackson actually understands 'underscored' properties out of the box - no need for special annotations. See the test cases below.

Regarding 'Mongo java driver' my assumption is applying @BsonProperty annotation should force treating a class method as property getter/setter.

But, what concerns particularly my usage scenarios, I came across more tough case - described here: https://stackoverflow.com/questions/64492659/mongodb-driver-sync-encoding-oldvalue-errored-with-cant-find-a-codec-for - and from the source code of java driver it seems I have to create own implementation of CodecRegistry to handle it. This means handling of underscored properties will be implemented 'by the way'.

 

public static class APojo {
    String _id = "id";
 
    public String get_id()          { return _id; }
    public void set_id(String _id)  { this._id = _id; }
}
 
@Test
public void testJacksonMapping() {
    ObjectMapper mapper = new ObjectMapper();
    APojo apojo = new APojo();
    Map<?,?> map = mapper.convertValue(apojo, Map.class);
 
    assertEquals("id", map.get("_id"));
}

 

Comment by Jeffrey Yemin [ 26/Oct/20 ]

I don't think the driver's POJO serializer should natively handle "_" specially, as this is not a standard Java convention.  

However, this does seem like something we should be able to get to work with a little extra declarative configuration, and while looking for inspiration I came across Jackson's JsonGetter and JsonSetter annotations.  There is a tutorial on them here: https://www.baeldung.com/jackson-annotations.

Would something like that suit your purpose?

Comment by Xtra Coder [ 21/Oct/20 ]

I tried to check if is possible to achieve my 'private' goals via "sub-classing or something" and it seems to be not possible - stack trace until `PropertyReflectionUtils.isGetter` goes through number of static and private methods over final classes. I could not find any place to 'plug in'

As a side note - I've noticed a not-so-good thing, which cause useless polluting of garbage collector
https://github.com/mongodb/mongo-java-driver/blob/r4.1.1/bson/src/main/org/bson/internal/ProvidersCodecRegistry.java#L39

    public <T> Codec<T> get(final Class<T> clazz) {
        return get(new ChildCodecRegistry<T>(this, clazz));
    }

Each invocation of 'get()', which by its semantics typically not do anything 'write-like', actually for each call generates instance of `new ChildCodecRegistry`, which in 99.999% of cases will be thrown away within consequent `get(final ChildCodecRegistry<T> context)`

Comment by Xtra Coder [ 21/Oct/20 ]

Note: additionally to `_ id` my code has some other properties prefixed with ` _ ` - to intentionally distinguish 'system' fields from 'business' field, for example '_created', '_updated'. This is defined in interfaces implemented by POJOs and is also stored that way in database.

To avoid need to annotate such fields with '@BsonProperty' across all the code, it has sense to 'nativelly' support that in codecs code, e.g.: 

static boolean isPropertyAccessor(Method method, String prefix) {
    String name = method.getName();
    return name.length() > prefix.length()
        && name.startsWith(prefix)
        && ('_' == name.charAt(prefix.length()) 
            || Character.isUpperCase(name.charAt(prefix.length()))
            || method.isAnnotationPresent(BsonId.class)
            || method.isAnnotationPresent(BsonProperty.class));
}

Comment by Jeffrey Yemin [ 21/Oct/20 ]

Hi xtracoder@gmail.com sorry to hear you're having trouble.

The current behavior is by design. Is it possible for you to change the method names to getId and setId?

Comment by Xtra Coder [ 21/Oct/20 ]

Test case: replace this demo code

https://github.com/mongodb/mongo-java-driver/blob/r4.1.1/driver-sync/src/examples/tour/Person.java#L25

 

public final class Person {
    private ObjectId id;

to

public final class Person {
    public String _id;
 
    @BsonId public String get_id()                 { return _id; }
    @BsonId public void   set_id(final String _id) { this._id = _id; }

 

 

Generated at Thu Feb 08 09:00:38 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.