(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
- Grails Plugin Page
- 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 s
et()
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, s
et()
does the following:
- Retrieve the
Smartionary
that exists by the name of fruits .
- If it does not exist, create it.
- 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 s
et()
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:
- Enforces awareness of value modification
- 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 exists | Has SmartionaryEntries | Returns |
---|
| | 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
gettingThey are exactly like their
Map
counterpart, with one exception.
Method | example |
---|
contains | Smartionary.contains('fruits', 'apple') |
containsKey | Smartionary.containsKey('fruits', 'a') |
size | Smartionary.size('fruits') |
The exception to this rule is the
size()
method:
Condition | Result |
---|
Smartionary does not exist | -1 |
Smartionary exists, but SmartionaryEntries do not | 0 |
Otherwise | Normal 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.