(Quick Reference)

Smartionary Plugin - Reference Documentation

Authors: Aaron Brown

Version: 1.0.0

1 Introduction

The Smartionary plugin for Grails provides a simple structure for storing and retrieving information to and from a Domain with emphasis on treating it like a Map.

The goal of the plugin is to make it easy and convenient for developers and non-developers to be able to create, update, delete, and access bits of information.

In a production environment, it may be necessary to have certain criteria or sets of information be accessible to administrative or management roles, while at the same time be available to the developer in a progremmatically convenient way.

With the Smartionary, required information can be put into the domain by the developer during the development cycle. Once in production, if any modification of the present data needs to be made, a maintaner (not necessarily the developer) is able to alter the information without the need for downtime.

This guide is intended to describe usage and best-practices for the plugin. There are no configurations necessary, however the Datasource and, if used, the Spring Core Security Plugin (Specifically, controllerAnnotations.staticRules) should be carefully tested to ensure the plugin is interacting with them appropriately (*this has not been explicitly tested*).

The term Smartionary is a bit overloaded (unfortunately).

When referring to me.sudofu.smartionary.domain.Smartionary, which is a Domain class, it will be referred to as such (i.e. Smartionary domain).

When referring to me.sudofu.smartionary.Smartionary, which is the primary way to use the plugin, it will be referred to simply as the Smartionary or Smartionary interface.

Resources

  1. Grails Plugin Page
  2. Source

1.1 Overview

This section will discuss the plugin's structure. There is no configuration necessary. However, bear in mind the considrations of the Datasource and Spring Core Security Plugin.

Artifacts

Controllers

The plugin comes with two Controllers:

me.sudofu.smartionary.controller.SmartionaryController
me.sudofu.smartionary.controller.SmartionaryEntryController

The URIs for these controllers are:

/smartionary

/smartionaryEntry /smartionary/entries

Domains

It is not necessary to directly use the Domains. This is only provided for clarification purposes.

See Smartionary Interface for primary usage.

The plugin comes with two Domains:

me.sudofu.smartionary.domain.Smartionary
me.sudofu.smartionary.domain.SmartionaryEntry

These domains are meant to emulate Map and Map.Entry.

Smartionary has many SmartionaryEntries, and nothing more. Therefore, the Map-like structure is limited to a single level (no nesting).

See best-practices for a possible workaround.

The following constraints apply to the Domains:

  • There can be no two Smartionaries with the same name.
  • Within a Smartionary there can be no two SmartionaryEntries with the same name.

Here is a visual structure of what this would look like:

Map myMap = [:]

myMap.myValue = "foo" | | | |------|--------|-----Smartionary.name | | |--------|-----SmartionaryEntry.key |-----SmartionaryEntry.value

The Smartionary Interface

The Smartionary Interface can be found in:

me.sudofu.smartionary.Smartionary

It is meant to be the primary way to interact with the Domains, as it gives very useful methods that follow the design philosphy of the plugin.

1.2 Changelog

Version 1.0.1

Version 1.0.0

  • Code Complete / Initial Release
    • Interaction with Spring Core Security Plugin has not been tested.
    • Interaction with custom Datasource configurations (i.e. multiple datasources) has not been tested.

2 Using the Smartionary Interface

From a progremmatic standpoint, the plugin tries to abstract the Smartionary domain into the overloaded Smartionary object. The Smartionary object provides an interface that abides by the following goals:
  • Be dyanamic
    • Automatically create Domains as necessary.
    • Update Domains that already exist.
  • Be flexible
    • Multiple ways to do the same thing.
    • Attempt to reduce the number of Exceptions that would occur.
  • Don't mind the illogical
    • Deleting a Domain that doesn't exist is just fine.
  • Pretend to be a Map
    • Treat interaction for the developer like they're working with a Map
    • Provide some Map-methods so they can be used without needing to work with an actual Map.

2.1 Setting

Creating and updating Domains is done via the set() method. There are several forms, but only the more complex (and flexible) ones will be discussed here.

Through all examples here use Strings, any Object can be used just like a Map. The caveat is that it gets converted to a String when placed in the Domain, and will be a String when retrieved.

See get() for details.

The primary strength of Smartionary is that it can convert a Map directly into a Smartionary domain:

Map toSmartionary = [
    a: "apple",
    b: "banana",
    c: "cantaloupe",
    d: "durian"
]

Smartionary.set(toSmartionary, 'fruits', 'A few fruits, by letter.')

That is a very straightforward approach, however just like when creating a Domain object, the method signature allows you to put the Map directly into the parameter body:

Smartionary.set(
    'fruits',
    'A few fruits, by letter',
    a: "apple",
    b: "banana",
    c: "cantaloupe",
    d: "durian")

In the above sample, the description was moved to be the second positional parameter. This makes it a bit more readable than the explicit form.

In the above case, set() does the following:

  1. Retrieve the Smartionary that exists by the name of fruits .
    • If it does not exist, create it.
  1. Replace the values in the corresponding SmartionaryEntries.
    • If it does not exist, create it.

Therefore, the same Smartionary domain can be updated with a successive call, and a subset of values can be replaced, and others can be added:

Smartionary.set(
    'fruits',
    c: "cherry",
    t: "tomato",
    w: "watermelon")

By using this method, it is impossible to encounter the unique naming constraints.

Due to the nature of setting, be very mindful if you wish to only update the description . For more information, read about setting Smartionary descriptons

Setting from JSON

An extension of the set() method is the fromJson() method. This provides a convenient way of converting a JSON string which would describe a set of parameters or criteria into an equivalent Smartionary.

String json = '{"a": "apple", "b": "banana", "c": "carrot", "d": "durian"}'

Smartionary.set('fruits', json, 'A few fruits, by letter.')

json = '{"c": "cherry", "t": "tomato", "w": "watermelon"}'

Smartionary.set('fruits', json)

Note that the order of the parameters differs. It is not shown here, but in all instances of set(), the smartionaryName is the first parameter with the exception of the example above. This is due to the way Groovy handles that particular method signature. {warn}

When working with JSON, an array such as the following is not supported:

'["apple", "banana", "cantaloupe", "durian"]'

This will throw an IllegalArgumentException.

null

Nested Maps and Lists inside the JSON will be converted to Strings, so this is not very appropriate for complex schemas.

2.1.1 Setting Descriptions

There are certain considerations to bear in mind when it comes to the descriptions of Smartionary and SmartionaryEntry. Both Domains allow for a description, which is primarily for when looking at the view. While developing, it is best-practice to put some form of description with the instances, even though the Smartionary has no methods to retrieve them.

Smartionary Description

The description for a Smartionary can be set using any form of the set() method. There is no harm in omitting the description; if the domain already has one, it will keep it.

SmartionaryEntry Description

The description for a SmartionaryEntry can only be updated (via the Smartionary) by the set() methods (or fromJson()). The explicit methods are fairly starightforward, but for the named-parameter signature there is a specific way:

Smartionary.set(
    'fruits',
    a: "apple",
    b: "banana",
    c: "cantaloupe",
    d: "durian",
    smartionaryDescriptions: [
        a: "Can be green, yellow, or red; sour or sweet.",
        b: 'Grows in bunches, called "hands."',
        c: "A kind of melon with a lattuce skin.",
        d: "A spiney fruit like a pineapple, that's really stinky.",
        t: "A red, fragile bulb; debated to be a vegetable."
    ]
)

The smartionaryDescriptions key is sort of a "reserved" key for this method. It follows a simple rule: only update the keys that appear in the outer Map . In this example, the key t does not appear in the outer Map, therefore, even if a SmartionaryEntry exists for t, it will not be updated.

The rationality behind this is two-fold:

  1. Enforces awareness of value modification
  2. Prevents accidental and ambiguous behavior when dealing with null.

This behavior applies to all versions of set() (as well as fromJson()). If it is necessary to update only the description, it is best and safer to use the web-interface.

Should you choose to update the descriptions through the Smartionary, be aware that if you pass null for the value of that key, then that is what will be written to the Domain.

2.2 Getting

Accessing the information in a Smartionary domain progremmatically is done via the get() method.

The get() method does not automatically create a Smartionary domain if it does not exist (that can only be done by the set() methods).

Map data = Smartionary.get('fruits')

Depending on the state of the Domain, the following conditions apply:

Smartionary existsHas SmartionaryEntriesReturns
  null
 [:]
a=apple, b=banana, c=cantaloupe, d=durian

The philosophy behind the plugin is that get() is used more frequently than set(). While it may be tempting to cache or somehow store the information, that ruins the advantages of being able to change the values of a Smartionary while in production, on-the-fly. Therefore, the following is not best practice:

class MyClass {
    Map myInfo = Smartionary.get('myInfoParams')

private final static MyInfo INSTANCE = new MyInfo()

private myInfo() { }

static getInstance() { return INSTANCE } }

The contents of myInfo can only be updated on application restart.

Getting as JSON

Just like withJson() there is a method for retrieving a Smartionary as a JSON string directly. It uses the internal Groovy JsonBuilder to fascilitate this:

try {
    Smartionary.getAsJson('fruits')
} catch (IllegalArgumentException e) {
    // The Smartionary may not exist.
} catch (JsonException e) {
    // Something went awry.
}

Please note the try/catch statement.

Since the JsonBuilder is used, by passing true (or explicitly false) you can specify that the String should (not) be pretty-formatted:

try {
    Smartionary.getAsJson('fruits', true)
} catch (IllegalArgumentException e) {
    // The Smartionary may not exist.
} catch (JsonException e) {
    // Something went awry.
}

2.3 Map-like Interface

The Smartionary comes with several basic Map-like functions, so that some operations can be performed directly, rather than making it necessary to retrieve Map through getting

They are exactly like their Map counterpart, with one exception.

Methodexample
contains
Smartionary.contains('fruits', 'apple')
containsKey
Smartionary.containsKey('fruits', 'a')
size
Smartionary.size('fruits')

The exception to this rule is the size() method:

ConditionResult
Smartionary does not exist-1
Smartionary exists, but SmartionaryEntries do not0
OtherwiseNormal size() behavior for Map.

3 Guidelines and Best-Practice

There are a few guidelines / best-practices to bear in mind while working with the plugin.

Simple (Read: Limited)

The plugin is very simple, especially in an early version. The Smartionary domain does not support nested mapping, interpreting Lists, or complex datatypes. Therefore, it is limited to the very basic datatypes.

That being said, here is a little trick that might at least make nested mapping somewhat doable:

Smartionary.set(
    'computer.officeTerm1',
    'Specs of Office Terminal #1',
    cpu: 'Widget w12',
    'cpu.cores': 4,
    'cpu.speed': 2.2,
    memory: 4096,
    'memory.socket': 4,
    'memory.type': 'DDR3',
    'disk.1': 'OceanWall',
    'disk.1.capacity': 1099515288913 
    'disk.1.type': 'SSD',
    'disk.1.mounted': true,
    'disk.2.mounted': false
    smartionaryDescriptions: [
        'cpu.speed': 'GHz',
        'disk.1.capacity': 'In Bytes (~1 TB)',
        'disk.2.mounted': 'Unused'
    ]
)

Map computerInfo = Smartionary.get('computer.officeTerm1')

println computerInfo.'disk.1.capacity'

A bit cludgy, but doable if necessary.

List and nested mapping may be supported in later versions.

Normalizes

Anything goes in, but all that comes out are Strings. This is powerful, as it means that it automatically "normalizes" all of the data; at the same time it may be a bit tedious as there is no way to automatically convert that data back when getting.

Groovy has several String operations (such as isInteger() and toInteger()) that make converting this data back a little less painful.

Data is Volatile

The plugin cannot assume the type of environment that it will be used in. It can only try to make programming around it a little easier. However, there may be a case where data is accidentally altered or deleted from the Domains, which could break functionality.

That is not an issue with plugin, but rather the nature of the data management system that this plugin tries to convenience. The best that can be done (in any regard) is to either develope a solution that could become aware of and try to mitigate the effects of entirely breaking, or use Spring Core Security Plugin to make sure the web-interface is under supervision.

A nice idea is put information in the description that might indicate whether or not a certain piece of data is requried or important for key functionality, and how to go about handling its modification.

Use Descriptions

On the above note, use descriptions liberally. Descriptions can be done either by code or via the web-interface. It was made to hold as much data as possible, and supports readibility with newlines in both viewing and editing in the web-interface.

set() With Care; get() With Abandon

When setting, be wary of the values you are using. null is a perfectly valid value, and so do not commit the folly of trying to update just a description of a SmartionaryEntry progremmatically.

One of the plugin's advantages is that data can be altered during runtime without the need to restart. Therefore, it is against this advantage to design a solution such that you get only once. If that is the use-case, then there is no difference between the plugin and the grails-app/conf/Config.groovy file, or hard-coding that data.