[JAVA-3902] POJO mapping - Setter can't be found if property is capitalized like xAxis Created: 08/Dec/20  Updated: 10/Oct/22

Status: Backlog
Project: Java Driver
Component/s: POJO
Affects Version/s: None
Fix Version/s: None

Type: Improvement Priority: Minor - P4
Reporter: Maxime Beugnet Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

A user reached out to me because his piece of code wasn't working as expected.

https://gist.github.com/pavangayakwad/5adbece8e11b26508a1338dc032c60ae 

He had a POJO with a few fields and 2 of them were named "xAxisColumn" and "yAxisColumn".

All the fields were mapped correctly - he could write them to MDB but not retrieve these 2. 

His IDE and mine (IntelliJ) generated the getter and setter like so: 

getxAxisColumn and setxAxisColumn (note the lower case X).

And we are expecting getXAxisColumn (upper case X) apparently which respect the CamelCase - but apparently this is not the correct answer as it's not respecting the JavaBeans API specification from 1997

This isn't a big deal per say - and there is an easy workaround. But this is really confusing and not an easy bug to find / understand as it's based on introspection if I understand this correctly.

Here is a piece of code to illustrate better what I'm saying.

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
 
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
 
public class MappingPOJOFailed {
 
    public static void main(String[] args) {
        ConnectionString connectionString = new ConnectionString("mongodb://localhost");
        CodecRegistry pojoCodecRegistry = fromProviders(PojoCodecProvider.builder().register(Good.class, Bad.class).build());
        CodecRegistry codecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), pojoCodecRegistry);
        MongoClientSettings clientSettings = MongoClientSettings.builder()
                                                                .applyConnectionString(connectionString)
                                                                .codecRegistry(codecRegistry)
                                                                .build();
        try (MongoClient mongoClient = MongoClients.create(clientSettings)) {
            MongoDatabase db = mongoClient.getDatabase("test");
            MongoCollection<Good> goodColl = db.getCollection("good", Good.class);
            MongoCollection<Bad> badcoll = db.getCollection("bad", Bad.class);
 
            goodColl.insertOne(new Good().setXAxis("I'm here!"));
            System.out.println(goodColl.find().first().getXAxis()); // This line prints "I'm here!"
 
            badcoll.insertOne(new Bad().setxAxis("I'm NOT here!"));
            System.out.println(badcoll.find().first().getxAxis()); // This line SHOULD print "I'm NOT here!" but I get "null".
            goodColl.drop();
            badcoll.drop();
        }
    }
 
    public static class Good {
        private String xAxis;
 
        public String getXAxis() {
            return xAxis;
        }
 
        public Good setXAxis(String xAxis) {
            this.xAxis = xAxis;
            return this;
        }
    }
 
    public static class Bad {
        private String xAxis;
 
        public String getxAxis() {
            return xAxis;
        }
 
        public Bad setxAxis(String xAxis) {
            this.xAxis = xAxis;
            return this;
        }
    }
}

Ouput:

I'm here!
null



 Comments   
Comment by Ross Lawley [ 09/Dec/20 ]
  • The driver by default follows the general Java convention for getters and setters for method names and their associated property names.
  • In the case of two conflicting properties xAxis and XAxis as they don't follow the established convention, the existing behaviour is reasonable, logical and explainable. The driver can't start mapping getXAxis to XAxis as it would break all existing users.

The logic for determining getters is as follows:

    private static final String GET_PREFIX = "get";
 
    static boolean isGetter(final Method method) {
        if (method.getName().startsWith(GET_PREFIX) && method.getName().length() > GET_PREFIX.length()) {
            return Character.isUpperCase(method.getName().charAt(GET_PREFIX.length()));
        }
        return false;
    }

That means getxAxis is never considered to be a getter, this would force the user to manually add a propertyModel for xAxis. There would also be a conflict as the default naming system maps getXAxis to xAxis. While it is a contrived edge case having two conflicting properties like this and ultimately, the PojoCodec can support that. It does however, require user intervention and a good knowledge of how to build a PojoCodec for a class to be able achieve that.

So there are two things to consider:

1) Adding support for getters/setters like getxAxis()
2) Adding support for properties xAxis & XAxis that after normalization map to the same property name.

Comment by Maxime Beugnet [ 08/Dec/20 ]

I had a typo in my comment above. I fixed it.

Comment by Ross Lawley [ 08/Dec/20 ]

Thats a fun example! The PojoCodec by default relies on the camelCase convention, this means properties / methods start with a lower cased letter.

So getPropertyName() maps to propertyName. The above case with both getxAxis() and getXAxis() isn't unexpected - it follows the above convention. Without further configuration the PojoCodec only sees the getXAxis() method and maps that to a xAxis property. The method getxAxis() is totally ignored by the PojoCodec as are any other non conventionally named methods.

Comment by Maxime Beugnet [ 08/Dec/20 ]

 EDIT: I had a typo in it. I fixed it.

This example is even more problematic I think: 

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
 
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
 
public class MappingPOJOFailed {
 
    public static void main(String[] args) {
        ConnectionString connectionString = new ConnectionString("mongodb://localhost");
        CodecRegistry pojoCodecRegistry = fromProviders(PojoCodecProvider.builder().register(Good.class).build());
        CodecRegistry codecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), pojoCodecRegistry);
        MongoClientSettings clientSettings = MongoClientSettings.builder()
                                                                .applyConnectionString(connectionString)
                                                                .codecRegistry(codecRegistry)
                                                                .build();
        try (MongoClient mongoClient = MongoClients.create(clientSettings)) {
            MongoDatabase db = mongoClient.getDatabase("test");
            MongoCollection<Good> goodColl = db.getCollection("good", Good.class);
 
            goodColl.insertOne(new Good().setxAxis("I'm the lower case one.").setXAxis("I'm the upper case one."));
            System.out.println(goodColl.find().first().getxAxis());
            System.out.println(goodColl.find().first().getXAxis());
 
            goodColl.drop();
        }
    }
 
    public static class Good {
        private String xAxis;
        private String XAxis;
 
        public String getxAxis() {
            return xAxis;
        }
 
        public Good setxAxis(String xAxis) {
            this.xAxis = xAxis;
            return this;
        }
 
        public String getXAxis() {
            return XAxis;
        }
 
        public Good setXAxis(String XAxis) {
            this.XAxis = XAxis;
            return this;
        }
    }
}

Ouput: 

null
I'm the upper case one.

In the collection: 

{ "_id" : ObjectId("5fcf852f2d616e734cfce239"), "xAxis" : "I'm the upper case one." }

Definitely surprising for someone unaware of this edge case.

 

Comment by Jeffrey Yemin [ 08/Dec/20 ]

Thanks for the report. We'll have a look and see if there's anything we can do.

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