Building Bot. Part two: Mongo Codecs

I’m continuing to tell about Telegram bot. In the previous post I described a process of scrapping, in order to retrieve data. I save my data to MongoDB and during my work I faced with some difficulties. In this post, I’ll write about codecs, which let you encode end decode your BSON data.

When you use java-mongo-driver, writing documents is as easy as collection.insertOne(new Document("name", "Café Con Leche"));

But, what if you’d like to write your POJO to collection? The driver provides you with a codec interface, with quite a fiddly API and example on the website is too simple.

The main entity in my scrapper is Recipe, which consists of a couple of text fields and an array of Ingredients. The class looks like this (I’ll skip some fields and getters/setters for simplicity):

public class Recipe {
  String name;
  String link;
  List<Ingredient> ingredients;
  String backQuote;
}

Ingredient class is really simple and looks like this:

public class Ingredient {
  String name;
  String quantity;
}

First of all, we need to implement codec for the ingredient, as it’s a child of Recipe.

public class IngredientCodec implements Codec<Ingredient>{
  @Override
  public Ingredient decode(BsonReader reader, DecoderContext decoderContext) {
    reader.readStartDocument();
    String name = reader.readString("name");
    String amount = reader.readString("amount");
    reader.readEndDocument();
    return new Ingredient(name, amount);
  }

  @Override
  public void encode(BsonWriter writer, Ingredient value, EncoderContext encoderContext) {
    writer.writeStartDocument();
    writer.writeString("name", value.getName());
    writer.writeString("amount", value.getQuantity());
    writer.writeEndDocument();
  }

  @Override
  public Class<Ingredient> getEncoderClass() {
    return Ingredient.class;
  }
}

It seems simple for now when you encode you use BsonWriter to write BSON and when you decode you use BsonReader to read values and pass them to your object. Most important thing is that the order of your readings and writings should be the same. Now let’s look at RecipeCodec

public class RecipeCodec implements Codec<Recipe> {

  private CodecRegistry codecRegistry;

  public RecipeCodec(CodecRegistry codecRegistry) {
    this.codecRegistry = codecRegistry;
  }

  @Override
  public Recipe decode(BsonReader reader, DecoderContext decoderContext) {
    Recipe recipe = new Recipe();
    reader.readStartDocument();
    recipe.setId(reader.readObjectId("_id").toString());
    recipe.setName(reader.readString("title"));
    reader.readString("link");
    try {
      recipe.setBackQuote(reader.readString("backquote"));
    } catch (BsonSerializationException ignored){}
    finally {
      Codec<Ingredient> ingredientCodec = codecRegistry.get(Ingredient.class);
      List<Ingredient> ingredients = new ArrayList<>();
      reader.readStartArray();
      while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
        ingredients.add(ingredientCodec.decode(reader, decoderContext));
      }
      reader.readEndArray();
      recipe.setIngredients(ingredients);
    }
    reader.readEndDocument();
    return recipe;
}

  @Override
  public void encode(BsonWriter writer, Recipe value, EncoderContext encoderContext) {
    writer.writeStartDocument();
    writer.writeString("title", value.getName());
    writer.writeString("link", value.getLink());
    if(value.getBackQuote()!=null){
      writer.writeString("backquote", value.getBackQuote());
    }
    writer.writeStartArray("ingredients");
    for(Ingredient i: value.getIngredients()){
      Codec<Ingredient> ingredientCodec = codecRegistry.get(Ingredient.class);
      encoderContext.encodeWithChildContext(ingredientCodec, writer, i);
    }
    writer.writeEndArray();
    writer.writeEndDocument();
  }

  @Override
  public Class<Recipe> getEncoderClass() {
    return Recipe.class;
  }
}

The important thing here is that ‘backquote’ field is optional and MongoDB supports it, but when you start using driver and try to get this field, you get an error, so the only option is to use try-finally when you are trying to access optional field. Another important thing is that you always should have the same order. BsonReader and BsonWriter are streams so there is no random access to fields. On this example, you could see how to encode and decode array using CodecRegistry, which you should pass when you have a complex object.

Next step is to tell mongo driver that we want to use our new codecs to encode/decode objects. We need to implement CodecProvider

public class RecipeCodecProvider implements CodecProvider {

  @Override
  @SuppressWarnings("unchecked")
  public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
    if (clazz == Recipe.class) {
      return (Codec<T>) new RecipeCodec(registry);
    }else if (clazz == Ingredient.class){
      return (Codec<T>) new IngredientCodec();
    }
    return null;
  }
}

After this, we could init mongo driver with this codecs

CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
    CodecRegistries.fromProviders(new RecipeCodecProvider()),
    MongoClient.getDefaultCodecRegistry());
MongoClientOptions options = MongoClientOptions.builder()
    .codecRegistry(codecRegistry).build();
MongoClient mongoClient = new MongoClient(new ServerAddress(config.getDbHost(), config.getDbPort()), options);
database = mongoClient.getDatabase(config.getDbName());

Finally, we’re ready to work with our objects

MongoCollection<Recipe> recipes =  database.getCollection("recipes", Recipe.class);
Recipe recipe = recipes.find(new Document("_id", new ObjectId(id))).first();
recipes.insertOne(recipe);

Next time I’ll write about creating bot itself.