Code documentation
Development Tools
Code Structure
Techniques and Standards
Help and Web Site
How To
Functional Info
Background Info

JMRI Help:

Contents Index
Glossary FAQ

Donate to JMRI.org

JMRI Code: Patterns and Organization

JMRI has grown and evolved with time, and you can't always see the currently-preferred structure and patterns by looking at older code pieces.

This page attempts to describe the recommended structure and patterns, and point to examples of current best practices.

Names, NamedBeans, and Managers

The "NamedBean" concept is basic to JMRI. A NamedBean is a basic JMRI object that represents something, typically something like a specific Sensor or Turnout. Functionally, all the device object classes (Sensor, Turnout, ...) and their specific implementations (LnSensor, XNetTurnout, ...) inherit from the base NamedBean class.

Naming and Handles

To get access to a specific object (a NamedBean of a specific type with a specific name), you make requests of a manager: You ask a TurnoutManager for a specific Turnout. In turn, you access the managers through the common InstanceManager.

A user might want to reference a NamedBean via a user name, and in turn might want to change the specific NamedBean that user name refers to. "Yard East Turnout" might be "LT12" at one point, and later get moved to "CT5". To handle this, your code should use NamedBeanHandle objects to handle references to NamedBeans. They automate the process of renaming.

To do this, when you want to store a reference to a NamedBean, e.g. to remember a particular Sensor, Turnout, SignalMast, etc ask (through the InstanceManager) the NamedBeanHandlerManager to give you a NamedBeanHandle:


 NamedBeanHandle<Sensor> handle =
        InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(name, sensor);
      

where name is the String name that the user provided, either a system name or user name, and sensor is the particular Sensor object being stored. When you need to reference the sensor itself, just do


sensor = handle.getBean();

Please use getBean() every time you need to access the bean. Don't cache the reference from getBean(). That way, if somebody does a "move" or "rename" operation, the NamedBeanHandle will get updated and your next getBean() call will get the right reference.

Bean properties

NamedBeans usually have state, for example a Sensor may be Active or Inactive (or Unknown or Inconsistent). This state is represented by one or more Java Bean properties. Code in Java and Jython can use the PropertyChangeListener pattern to get notified when a given property changes. As an example, when a turnout is configured for a feedback sensor, the Turnout object registers itself as a change listener when the Sensor's state property changes, and updates the Turnout's "KnownState" property.

The available Bean properties are defined in the abstract base class usually, for example AbstractTurnout defines "CommandedState", "KnownState", "feedbackchange", "locked" and some more at the time of this writing. These properties are not system-dependent. Some of the properties are run-time only (e.g. state -- is the turnout thrown or closed?), while others (e.g. turnout feedback mode) are configuration settings, selected by the user and saved between sessions.

Editing and saving NamedBeans

NamedBeans are created and configured by the user using explicit actions. Most of the UI for these actions is in the jmri.jmrit.beantable package, using the generic BeanTable{Frame,Pane,Model} classes specialized for the particular type, for example in the TurnoutTableAction class. The configuration options present in the table and the edit dialog are specific to the type (Turnout) but not the system.

The beans with the configured options are persisted into the Configuration (and Panel) XML file when the user saves those. The persistence is handled by the system- and object-specific ManagerXml class, for example LnTurnoutManagerXml or OlcbTurnoutManagerXml, which heavily rely on shared code in AbstractTurnoutManagerConfigXML, but can introduce system-specific functionality and work together with the system- and object-specific manager (e.g. OlcbTurnoutManager) to achieve this.

The base class handles persisting the user settings that were entered via the BeanTable.

System-specific properties

Adding a system-specific property requires using a generic API, because the code in the jmrit.beantable package cannot depend on the jmrix.system-specific packages. All NamedBeans have a setProperty and getProperty method where arbitrary values can be saved for any string key. These properties are persisted into the XML file by the base class of the ManagerXml, so no code needs to be written for it. A variety of basic types can be chosen for the property value, such as Integer or Boolean, and will be correctly persisted and recovered upon load. Custom types might work if they have a toString() method and a constructor that takes only one String as argument and these correctly serialize and parse the data value.

To allow the user to edit these system-specific properties, a specific Manager can declare the set of supported properties by returning appropriately filled NamedBeanPropertyDescriptor objects from the getKnownBeanProperties method. This descriptor tells the BeanTable that additional columns need to be created, what type of data those columns will hold and what should be the column names (printed in the header). The system-specific columns are hidden by default from the user; the user needs to click a checkbox in the bottom row to show them; the checkbox only appears if there are system-specific properties. The column name has to be filled with a localized string that should come out of the respective Manager's Bundle.

Service Providers

Java provides a capability, using a "Service Provider Interface", that allows us to reduce the complexity of our code by having the code itself discover what pieces are available and need to be installed.
For background on this, see the tutorial sections on "Creating Extensible Applications" and "Introduction to the Service Provider Interfaces".

For example, by annotating a class with


      @ServiceProvider(service = PreferencesManager.class)
      
the JMRI Preferences System automatically will discover that the class uses the preferences and should be hooked up. This means that we don't have to modify the Preferences classes to look up each new class using them, and that we can (eventually) more incrementally build and distribute JMRI.

Available patterns (links are to the JavaDoc for the interface or class specifying the functionality):

ConnectionTypeList
HttpServlet
(Note this is a Java-defined class, not a JMRI-defined interface)
InstanceInitializer
Provides a way for the JMRI InstanceManager to create an instance of the class when one is requested
JsonServiceFactory
PreferencesManager
PreferencesPanel
SignalMastAddPane
Provide a type-specific pane used to add/edit the information in a SignalMast concrete object
StartupActionFactory
StartupModelFactory
WebManifest
WebServerConfiguration

To see more on the places where SPI can be used to insert functionality into JMRI, see the Plug-in page.

Classes that provide SPI also have to be registered with the system so they can be found. JMRI does this with entries inside files in the target/classes/META-INF/services/ directory. These entries are created automatically during the JMRI build process from the annotations in the source files. JMRI then packages those into the appropriate level of jmri.jar file, where they will eventually be found and acted on.

To access them:

 java.util.ServiceLoader.load(OurServiceClass.class).forEach((ourServiceObject) -> {
     // access the service object via ourServiceObject
});

JavaScript and TypeScript Code

JavaScript code for use by the web server should be put in the web/js directory, which is where our web pages are served from via the JMRI web server.

TypeScript code for use by the web server should be put in the web/ts directory. This will be compiled as needed by the ant typescript target. The results are put in the web/js directory.

To run the ant typescript target and compile the TypeScript files, a TypeScript compiler needs to be installed on the computer. For more information on TypeScript and how to install it, see the TypeScript web page.

JavaScript code can also be used for scripting. By default, these should be put into the jython/ directory, though of course people can run scripts from any location they choose.