Client-Side Transformation

October 10, 2018 | Sam Smith | java-cloudant python-cloudant nodejs-cloudant Modelling Encryption

Using Cloudant’s client libraries it is possible to transform fields in your client-side objects before the data is transmitted from the client to the server and to perform the reverse transformation when documents have been retrieved from the server.

There are many use cases where this may be useful for example:

It is also possible to perform transformations on the server-side, but there are situations where a client-side transformation may be preferable. For example, the locale to use when transforming a property is easily known on the device retrieving the document and a document may be retrieved multiple times across many regions. A second example is encryption; in the case of client-side encryption data can be transformed before it reaches the server, meaning data protection is controlled by the data owner and that the data is never visible to the service provider.


Below are some examples of how to implement client-side data transformation in the Cloudant client libraries for:

For these examples the transformation is reversing a name string and adding a prefix: Leonardo da Vinci -> Reversed:icniV ad odranoeL. The examples assume environment variables of:

For any of the examples after the test document has been written we can verify that what’s being stored in Cloudant is the transformed property by using cURL in a terminal:

curl -u $USERNAME:$PASSWORD https://$ACCOUNT_NAME.cloudant.com/testdatabase/test | jq .
{
    "_id":"test",
    "_rev":"1-4674789f5189ac5dc132c25968f4fafd",
    "name":"Reversed:icniV ad odranoeL",
    "isInventor": true
}

java-cloudant

import com.cloudant.client.api.ClientBuilder;
import com.cloudant.client.api.CloudantClient;
import com.cloudant.client.api.Database;
import com.cloudant.client.api.model.Document;
import com.cloudant.client.api.model.Response;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;

import java.lang.reflect.Type;

public class ClientSideTransformDemo {

    /**
     * Runnable main method demonstrating the creation of a document object with a field annotated
     * to be transformed. The document is written to the remote database. It is retrieved using a
     * different POJO type to demonstrate that the field is not detransformed in that case. It is
     * retrieved again with the correct POJO type to demonstrate the successful detransformation.
     *
     * @param args not used
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        CloudantClient c = ClientBuilder.account(System.getenv("ACCOUNT_NAME"))
                                        .username(System.getenv("USERNAME"))
                                        .password(System.getenv("PASSWORD"))
                                        .build();
        String testDbName = "testdatabase";
        Database db = c.database(testDbName, true);
        try {
            // Create a new POJO document with a transformed field and save it to the database
            DocWithTransformFields testDoc = new DocWithTransformFields();
            testDoc.setId("test");
            testDoc.name = "Leonardo da Vinci";
            testDoc.isInventor = true;
            Response r = db.save(testDoc);
            System.out.println(r);
            if (r.getStatusCode() / 100 != 2) throw new Exception("Couldn't write document.");

            // Retrieve the doc from the database as the wrong type
            JsonObject d = db.find(JsonObject.class, "test", r.getRev());
            System.out.println("JsonObject view of doc is transformed:");
            System.out.println(d.toString());
            if (d.get("name").equals(testDoc.name))
                throw new Exception("Name should be reversed.");

            // Retrieve the doc from the database as the correct type
            DocWithTransformFields d2 = db.find(DocWithTransformFields.class, "test", r.getRev());
            System.out.println("DocWithTransformFields detransforms doc:");
            System.out.println(d2.toString());
            if (!d2.name.equals(testDoc.name))
                throw new Exception("Name should be normal.");
        } finally {
            c.deleteDB(testDbName);
        }
    }

    /**
     * <P>
     * Class demonstrating how a POJO can have fields annotated with a JsonAdapter to invoke a
     * custom serializer that can transform the JSON value of a field.
     * </P>
     * <P>
     * Notes:
     * </P>
     * <UL>
     *     <LI>Although for simplicity this POJO extends com.cloudant.client.api.model.Document it
     *     is not necessary to do so.</LI>
     *     <LI>In this case the adapter TransformingSerializer demonstrates a JSON
     *     property value reversal.</LI>
     * </UL>
     */
    public static class DocWithTransformFields extends Document {

        // Annotate the name field with the transforming serializer as we want the name to be reversed
        @JsonAdapter(TransformingSerializer.class)
        String name;

        // Another field in the document that does not need to be transformed
        boolean isInventor;

        @Override
        public String toString() {
            return "{" +
                    "\"_id\":\"" + getId() + "\"," +
                    "\"_rev\":\"" + getRevision() + "\"," +
                    "\"name\":\"" + name + "\"," +
                    "\"isInventor\":" + isInventor +
                    "}";
        }
    }

    /**
     * Note this demonstration implementation converts the JSON value to a string
     * and reverses it before adding a prefix.
     *
     * @param <T> the type of the field
     */
    public class TransformingSerializer<T> implements JsonSerializer<T>, JsonDeserializer<T> {

        /**
         * Deserialize a JSON property to the specified type typeOfT applying
         * the necessary field detransformation.
         *
         * @param json The JSON data being deserialized.
         * @param typeOfT The type of the Object to deserialize to.
         * @param context Context for deserialization.
         * @return a deserialized object of the specified type typeOfT which is a subclass of T.
         * @throws JsonParseException if JSON is not in the expected format of typeofT.
         */
        @Override
        public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            JsonElement toDeserialize = json;
            // The transformed value always becomes a string in JSON
            if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) {
                toDeserialize = new JsonParser().parse(detransform(json.getAsString()));
            }
            return context.deserialize(toDeserialize, typeOfT);
        }

        /**
         * Serialize a src field of type T to a reversed string for JSON representation.
         *
         * @param src the object that needs to be converted to Json.
         * @param typeOfSrc the actual type (fully genericized version) of the source object.
         * @param context Context for serialization.
         * @return a JsonElement corresponding to the specified object.
         */
        @Override
        public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
            return new JsonPrimitive(transform(context.serialize(src, typeOfSrc).toString()));
        }

        private static final String _PREFIX = "Reversed:";

        /**
         *
         * @param text string form of JSON value to be reversed
         * @return reversed and prefixed string
         */
        private String transform(String text) {
            return _PREFIX + new StringBuilder(text).reverse().toString();
        }

        /**
         *
         * @param transformedText to detransform
         * @return original text
         */
        private String detransform(String transformedText) {
            String reversedText = transformedText.substring(_PREFIX.length(), transformedText.length());
            return new StringBuilder(reversedText).reverse().toString();
        }
    }

}

python-cloudant (requires cloudant>=2.10.0)

Passing a custom JSON encoder and decoder into the library gives us the ability to intercept data being sent to and from the server. This will allow for seamless transformation and detransformation of document data as required.

The demo below will transform/detransform only those values whose key is name.

import json

class DocTransformEncoder(json.JSONEncoder):

    _PREFIX = 'Reversed:'

    @staticmethod
    def demo_transform(s):
        return DocTransformEncoder._PREFIX + s[::-1]

    def encode(self, o):
        if 'name' in o:
            o['name'] = self.demo_transform(o['name'])

        return super(DocTransformEncoder, self).encode(o)


class DocDetransformDecoder(json.JSONDecoder):

    @staticmethod
    def demo_detransform(s):
        return s[len(DocTransformEncoder._PREFIX):][::-1]

    def decode(self, s, **kwargs):
        data = super(DocDetransformDecoder, self).decode(s, **kwargs)

        if 'name' in data:
            data['name'] = self.demo_detransform(data['name'])

        return data

More details on writing custom encoders and decoders can be found here.

We can now instantiate the Cloudant client. It should look something like this:

import os
from cloudant.client import Cloudant

client = Cloudant(os.environ['USERNAME'], os.environ['PASSWORD'], account=os.environ['ACCOUNT_NAME'], connect=True)

Let’s create a new database:

db = client.create_database('testdatabase')

Now we can write a document. Notice that we’re specifying our custom encoder and decoder here.

from cloudant.document import Document

doc = Document(db, 'test', encoder=DocTransformEncoder, decoder=DocDetransformDecoder)
doc['name'] = 'Leonardo da Vinci'
doc['isInventor'] = True
doc.save()

You can use the cloudant.Document object as normal so long as you’ve specified your custom encoder and decoder. So fetching our data back is simple:

doc2 = Document(db, 'test', encoder=DocTransformEncoder, decoder=DocDetransformDecoder)
doc2.fetch()

print(doc2['name'])  # => 'Leonardo da Vinci'
print(doc2['isInventor'])  # => True

nodejs-cloudant

We can create a new TransformedDoc class that will override the toJSON method allowing us to transform any data necessary before it’s sent off to the server.

In addition, there’s a fromObject too. This allows responses from the server to detransformed ready for the client to use.

class TransformedDoc {
  constructor(o) {
    Object.assign(this, o);
  }

  toJSON() {
    if ('name' in this) {
      var cpy = Object.assign({}, this);
      cpy.name = TransformedDoc.prefix() + cpy.name.split('').reverse().join('');
      return cpy;
    }
    return this;
  }

  static prefix() {
    return 'Reversed:';
  }

  static fromObject(o) {
    if ('name' in o) {
      o.name = o.name.substring(TransformedDoc.prefix().length).split('').reverse().join('');
    }
    return new TransformedDoc(o);
  }
}

Let’s create a Cloudant client and see it in action:

const Cloudant = require('@cloudant/cloudant');

const cloudant = new Cloudant({
  account: process.env.ACCOUNT_NAME,
  username: process.env.USERNAME,
  password: process.env.PASSWORD,
  plugins: [ 'cookieauth', 'promises' ]
});

(async () => {
  // Create our testdatabase:
  await cloudant.db.create('testdatabase');
  const db = cloudant.db.use('testdatabase');
  // Now we create our document:
  const doc = new TransformedDoc({ _id: 'test', name: 'Leonardo da Vinci', isInventor: true });
  // And send it to the server:
  return db.insert(doc);
})().then(resp => {
  // The doc was written
  console.log('written doc');
  const db = cloudant.db.use('testdatabase');
  // Fetch the written doc back again
  return db.get('test', {'rev': resp.rev});
})
.then(doc => {
  // To detransform the document we need to use our `fromObject` method like so:
  let detransformedDoc = TransformedDoc.fromObject(doc);
  console.log(detransformedDoc.name); // => 'Leonardo da Vinci'
  console.log(detransformedDoc.isInventor); // => true)
  return new Promise(resolve => resolve());
})
.catch(err => console.log('something went wrong:', err.message));