JMRI Code: XML Persistance
JMRI uses XML for persisting internal structures, especially when storing the preferences and panel files.
XML persistance is done via some explicitly written code. Basically, certain classes register themselves with a instance of the "ConfigureManager". Normally, that will be the implementation that stores to and loads from XML files: jmri.configurexml.ConfigXmlManager. When it's time to store, the ConfigureXmlManager is told to do it. It goes through the registered objects and finds the persisting class responsible for storing the object. E.g. class a.b.Foo will have the class a.b.configurexml.FooXml located. If that class is found, it's told to store the Foo object, and it adds Xml content to a JDOM document to do that. If it's not located, an error message is issued.
On load, an XML file is read by the manager. Each element is examined for a "class" attribute. If found, that class is loaded and handed the element to process. Etc.
Although the basic structure is cleanly separated, the code with the *Xml classes tends to have a lot of replication and special case. To keep that all sane, we do a lot of unit and CI testing on it.
Example
A LightManager knows about Lights.
There are lots of concrete classes implementing the Light
interface:
- jmri.jmrix.loconet.LnLight
- jmri.jmrix.cmri.serial.SerialLight
- jmri.jmrix.powerline.SerialLight
There are also multiple LightManager concrete classes to handle them:
- jmri.jmrix.loconet.LnLightManager
- jmri.jmrix.cmri.serial.SerialLightManager
- jmri.jmrix.powerline.SerialLightManager
Each type of manager is stored and loaded via a persistance class, who is found by looking the a class with "Xml" appended to the name, in a "configurexml" direct subpackage:
- jmri.jmrix.loconet.configurexml.LnLightManagerXml
- jmri.jmrix.cmri.serial.configurexml.SerialLightManagerXml
- jmri.jmrix.powerline.configurexml.SerialLightManagerXml
In the case of Light concrete classes, the code for persisting the managers directly stores and loads the individual lights. This is because (so far) a given manager only has one type of Light (e.g. LnLightManager only has to worry about LnLight). In cases where this is not true, e.g. SignalHeads which have multiple classes, there are persistance classes for the individual objects in addition to the manager.
Adding More Information to a Class
If you want to
store more state information, find the persisting class and
add code to it to create and read attributes or elements.
Perhaps the easiest way to do this is to create a sample
panel file with the objects you want to store in it:
<sensors class="jmri.jmrix.cmri.serial.configurexml.SerialSensorManagerXml" /> <sensor systemName="CS3001" /> </sensor> <sensors class="jmri.managers.configurexml.InternalSensorManagerXml" /> <sensor systemName="IS21" /> </sensors> <signalheads class="jmri.configurexml.AbstractSignalHeadManagerXml"> <signalhead class="jmri.configurexml.DoubleTurnoutSignalHeadXml" systemName="IH1P"> <turnout systemName="CT10" userName="1-bit pulsed green" /> <turnout systemName="CT2" userName="1-bit pulsed red" /> </signalhead> </signalheads>
Note the "class" attributes. They give the fully-qualified name of the class that can load or store that particular element. In the case of Sensors, we see there are two managers in use: One for C/MRI, and one for internal Sensors. For SignalHeads, there's only one manager, jmri.configurexml.AbstractSignalHeadManager persisted by jmri.configurexml.AbstractSignalHeadManager, but each particular SignalHead implementing class has it's own persisting class.
To e.g. add more data to a sensor object, the jmri.jmrix.cmri.serial.configurexml.SerialSensorManagerXml and jmri.managers.configurexml.InternalSensorManagerXml classes would have to be modified. This is where all the code to transfer to and from the stored form should go; don't add code in e.g. primary classes SerialSensorManager and InternalSensorManager to translate to or from the stored form. This keeps the main classes internal and flexible, allowing the persistance to be worked on (and tested and debugged!) separately.
Boolean values should be stored as the strings "true" and "false".
The
getAttributeBooleanValue
,
getAttributeIntValue
, and
getAttributeIntegerValue
methods provide a simple way of parsing input and handling errors.
Although much of the early code stored information in attributes, consider putting the data being stored in elements instead. This makes for simpler XML Schema and XSLT definitions, and the structured nature can be easier for humans to read.
If you do add new attributes or elements, don't forget to update the format definition, see below.
Note that in some cases, there's an inheritance relationship amoung the persisting classes that can help. For example, the LocoNet LnSensorManagerXml class inherits from jmri.configurexml.AbstractSensorManagerConfigXML, which does almost all the work of storing and loading sensors.
Handling Errors
If theres a parsing error, exception, etc, it should be reported via the ErrorHandler. This accumulates reports and presents them (in a almost-useful way) to the user when appropriate.
For an example of how to do that, see the getAttributeBooleanValue
method.
Persisting enum values
The "EnumIO" tool built into AbstractXmlAdapter that reads and writes enums via the names of their elements.You add a member in your configureXml load/store class:
static final EnumIO<MyEmum> enumMap = new EnumIoNamesNumbers<>(MyEnum);
Then to store your value, in this example from the
myEnumValue
variable, you pass the enum value through that map element in your store
method:
element.setAttribute("name", "" + enumMap.outputFromEnum(myEnumValue));
Storing into an element is similar To restore the value on load:
myEnumValue = enumMap.inputFromAttribute(element.getAttribute("name"));
There are several variations available, please see the Javadoc. Briefly:
- EnumIoNames - just stores and loads the enum element names.
- EnumIoNamesNumbers - stores and loads the enum element names. On load, if it encounters a small integer value n, i.e. from an older file format, that will be translated to the nth enum value.
- EnumIoOrdinals - just stores and loads via the enum's ordinal numbers. This provides complete load and store backwards compatibility with earlier versions of the file.
- EnumIoMapped - allows you to provide arbitrary mappings between enums and the values loaded and stored. It's possible to provide multiple load values that make to a single enum value, i.e. "3" and "Ralph".
Persisting References to NamedBeans
Classes should, but don't always, hold references to NamedBeans via NamedBean handles.If you're adding persistance for a class that doesn't do that, please update it before going further. That will save a lot of future trouble.
To store a NamedBeanHandle reference, just store the result of the getName()
method of the NamedBeanHandle. That's the name the user refers to it by.
To load a reference, retrieve that name, look up the corresponding NamedBean
(typically with get(String)
method of the corresponding manager)
and then create the NamedBeanHandle via the usual call:
InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(name, thing)
Class Migration
Sometimes, classes need to be moved to another package as part of code maintenance. Since the fully-qualified class name, including package name, has been written to files, if we just move the class it will break reading of those files (in addition to breaking any user-written code that might refer to them). To handle this:- Move the *Xml file to its new location, just below the class it's loading and storing
- Make an entry in the
java/src/jmri/configurexml/ClassMigration.properties
file to map the old location to the new location - Optionally, create an empty *Xml class in the old location that just inherits from the *Xml class in the new location (so that it will still work) and mark it deprecated. This keeps functional other people's code that e.g. might inherit from it. You can remove this after a decent interval.
Schema Management
JMRI controls XML semantics using XML Schema.
For example, layout information is stored in XML files as a "layout-config" element, whose contents are then defined via a schema file. These files are kept in the xml/schema directory.