Skip to content
This repository has been archived by the owner on May 1, 2020. It is now read-only.

Writing plugins for custom data types

Sam Bosley edited this page Jul 1, 2016 · 14 revisions

Supporting custom data types

In some cases, you may want to store data/objects in your database that aren't one of the basic SQLite types. Code generation plugins support handling model spec fields that aren't of one of the standard primitive data types. We will outline a simple example here, but for a complete plugin implementation, see the example JSON plugin that supports serializing arbitrary objects to JSON strings.

Implementing a plugin

Regardless of your custom data type, you will need to serialize it to one of the basic types supported by SQLite--a number, string, or byte[]. Your custom data column will therefore be represented under the hood by one of the standard Property types, e.g. StringProperty, BlobProperty, etc.

Your plugin will probably be easiest to write if you first create a class with two static methods, one for serializing your custom data type into a model and one for deserializing it. Here's the basic pattern:

// Assuming your custom data type is serialized to a String
public static class CustomDataPluginSupport {

    public static CustomData getCustomData(AbstractModel fromModel, StringProperty property) {
        // The transitory values are purposed as a cache, so deserialization only needs to 
        // happen once. If serialization and deserialization are cheap, you can probably 
        // skip caching.
        if (!model.checkTransitory(property.getName())) {
            CustomData myData = null;
            if (model.containsValue(property)) {
                myData = parseCustomDataFromString(model.get(property));
            }
            model.putTransitory(property.getName(), myData);
            return myData;
        }
        return (CustomData) model.getTransitory(property.getName());
    }

    public static void setCustomData(AbstractModel intoModel, StringProperty property, CustomData data) {
        String serializedData = null;
        if (data != null) {
             serializedData = serializeCustomDataToString(data);
        }
        model.set(property, serializedData);
        model.putTransitory(property.getName(), data); // Pre-warm the transitory cache
    }
}

Your code generator plugin will now be able to generate getter and setter code that delegates to these methods more easily.

processVariableElement

SquiDB code generator plugins can override the processVariableElement method to handle custom data types. The method signature looks like this:

public boolean processVariableElement(VariableElement field, DeclaredTypeName fieldType);

(VariableElement is used by the Java annotation processing tool to represent a field at compile time--refer to the Javadocs for more information. DeclaredTypeName is a proprietary representation of a class and its generic arguments at compile time designed for easy traversal.)

This method will be called once for each field in the model spec until some plugin returns true. By returning true, a plugin has declared that field as "handled", and it will not be passed to any more plugins. To make it easy for these factories to recognize the fields they should handle and prevent other plugins from accidentally claiming the fields first, we recommend creating an annotation that you can bundle with your plugin--users can annotate fields they want handled by your plugin with this annotation, and your factory can ignore all fields that are not so annotated:

// Custom annotation for your data type
@Target(ElementType.FIELD)
public @interface CustomDataProperty {}

A complete implementation of your Plugin subclass would look something like this:

public class CustomDataFieldPlugin extends Plugin {

    private static final DeclaredTypeName CUSTOM_DATA
        = new DeclaredTypeName("com.mypackage.CustomData");

    public CustomDataFieldPlugin(ModelSpec<?> modelSpec, PluginEnvironment pluginEnv) {
        super(modelSpec, pluginEnv);
    }

    @Override
    public boolean processVariableElement(VariableElement field, DeclaredTypeName fieldType) {
        if (field.getAnnotation(CustomDataProperty.class) == null ||
                !CUSTOM_DATA.equals(fieldType)) {
            return false;
        }
        modelSpec.addPropertyGenerator(
            new CustomDataPropertyGenerator(modelSpec, field, fieldType, utils));
        return true;
    }
}

PropertyGenerator

PropertyGenerator subclasses are responsible for writing the actual code that goes into your model classes. While you can implement your own PropertyGenerators from scratch, we recommend subclassing on of the built-in BasicPropertyGenerator classes that have default implementations of nearly all the important methods. A BasicPropertyGenerator subclass exists for each basic type--BasicStringPropertyGenerator, BasicLongPropertyGenerator, etc., so you can just pick the one that corresponds to the data type you're serializing your custom data with and start with that.

Let's continue our CustomData example. We've already decided to serialize our data to a String, so we'll subclass BasicStringPropertyGenerator. Then, there are a few methods we'll need to override: registerAdditionalImports, getTypeForGetAndSet, writeGetterBody, and writeSetterBody. The full implementation looks like this:

public class CustomDataPropertyGenerator extends BasicStringPropertyGenerator {

    private final DeclaredTypeName fieldType;
    private static final DeclaredTypeName CUSTOM_DATA_PLUGIN_SUPPORT
        = new DeclaredTypeName("com.mypackage.CustomDataPluginSupport");

    public CustomDataPropertyGenerator(ModelSpec<?> modelSpec, VariableElement field,
            DeclaredTypeName fieldType, AptUtils utils) {
        super(modelSpec, field, utils);
        this.fieldType = fieldType;
    }

    // Add imports required by this property here
    @Override
    protected void registerAdditionalImports(Set<DeclaredTypeName> imports) {
        super.registerAdditionalImports(imports);
        imports.add(CUSTOM_DATA_PLUGIN_SUPPORT);
    }

    // Defines the type passed/returned in get/set
    @Override
    protected DeclaredTypeName getTypeForAccessors() {
        return fieldType;
    }

    // Generates getter implementation using the static helper class from earlier
    @Override
    protected void writeGetterBody(JavaFileWriter writer) throws IOException {
        Expression body = Expressions.staticMethod(CUSTOM_DATA_PLUGIN_SUPPORT,
            "getCustomData", "this", propertyName).returnExpr();
        writer.writeStatement(body);
    }

    // Generates setter implementation using the static helper class from earlier
    @Override
    protected void writeSetterBody(JavaFileWriter writer, String argName) throws IOException {
        Expression body = Expressions.staticMethod(CUSTOM_DATA_PLUGIN_SUPPORT,
            "setCustomData", "this", propertyName, argName);
        writer.writeStatement(body);
        writer.writeStringStatement("return this");
    }
}

The generated getter and setter in the model class will look like this:

public CustomData getCustomDataField() {
    return CustomDataPluginSupport.getCustomData(this, CUSTOM_DATA_PROPERTY);
}

public MyModel setCustomDataField(CustomData data) {
    CustomDataPluginSupport.setCustomData(this, CUSTOM_DATA_PROPERTY, data);
    return this;
}

See also: