Plugins are generally used to add dynamically loaded modules to an application based on user defined configuration.
Our chemical calculator plugin structure is added to Marvin for the following reasons:
We developed a mechanism to handle these calculations in a uniform way. This common interface is utilized as a common java API for developers, as a command line tool and also in our graphical applications and applets MarvinSketch and MarvinView.
The implementation of the general plugin handling mechanism can be found in the
chemaxon.marvin.plugin
package. Our specific plugin implementations
are in the chemaxon.marvin.calculations
package.
Calculator plugins have a common base class: chemaxon.marvin.plugin.CalculatorPlugin
.
This base class implements the license handling, provides some helper functions for number
formatting and declares the abstract methods to be implemented by the specific plugin classes for
input molecule and calculation parameter setting, performing the calculation, and getting the results.
Apart from this main plugin class, our graphical applications and applets
MarvinSketch and MarvinView require a
plugin loader class that must be a subclass of
chemaxon.marvin.plugin.CalculatorPluginLoader
. This loader class is responsible for
the instantiation of the plugin and handles the plugin parameter setting through the graphical
user interface. It provides the parameter panel as a java.awt.Component
object, verifies and fetches the plugin parameters from this panel and returns them in a
java.util.Properties
object.
The calc command line tool uses
chemaxon.marvin.plugin.CalculatorOutput
to generate the plugin results in table form.
Unlike the chemaxon.marvin.plugin.CalculatorPlugin
and
chemaxon.marvin.plugin.CalculatorPluginLoader
classes, this class is not abstract:
it implements the default table output with one result row for each input molecule,
the molecule ID in the first column followed by the plugin results in the subsequent
columns. A specific output table format can be defined by subclassing this class.
To access a plugin by the java API you only need to use the public methods in the
chemaxon.marvin.plugin.CalculatorPlugin
class as shown in the
examples section of this manual.
The graphical plugin handling algorithm:
The central plugin handler class is chemaxon.marvin.plugin.CalculatorPluginManager
:
this reads the configuration, and adds the list of available plugins to the Tools
menu and the Tools/Options
submenu. The Tools
menu items
activate the plugins, while the Tools/Options
menu items invoke the plugin loaders and
display their parameter panel on a general options pane. When a plugin is activated, the
corresponding plugin loader object is asked for the parameters as a
java.util.Properties
object and sets them in the plugin object. Then the input
molecule (the current molecule in the sketcher/viewer) is set and the plugin calculation is run.
Finally, the calculation result is queried from the plugin, converted to string in the plugin
and displayed on the sketcher/viewer GUI or in a dialog box.
Both the cxcalc command line tool and the
graphical applications and applets require one or more configuration files that specify the
plugins available. For the configuration of the command line tool see the
Configuration File section in the
Calculator user manual. The plugin configuration for
MarvinSketch and MarvinView
is specified in one or more java properties file given in the toolfiles
application or applet parameter. If more than one file is specified, then the file names should be
separated by a ',' character (without spaces) and their contents will be merged.
The file names should be given relative to the CLASSPATH
. Marvin applets
load configuration files from the server computer. If no configuration file is given
then the default configuration file is read:
chemaxon/marvin/calculations/plugins.properties
. The configuration file syntax is
best shown by an example:
#<loader class name>$<menu/mnemonics>$<title>$<CHART>$<*> # only the class name mandatory, "CHART" means line-chart data, an ending '*' means preload plugin_1=chemaxon.marvin.calculations.ChargePluginLoader$Charge/Ch$Charge plugin_2=chemaxon.marvin.calculations.pKaPluginLoader$pKa/K$pKa
The property keys should be unique within one configuration file. If the key ends
with a number preceeded by a '_' character then that ending number is used to determine
the position of the plugin in the Tools
menu: the plugins are sorted
according to ascending ending number order, plugins with no ending number are put at
the end.
Each property value defines a plugin configuration by giving the following fields (fields are separated by '$' characters):
Tools
menu and the possible mnemonics characters
(these two are separated by a '/' character): the first possible mnemonic character is set
for menu mnemonic that is not already used by another plugin - if no such character exists
then no mnemonic is set
You can write a custom plugin for a chemical calculation that is not currently available
as a built-in plugin.
This section describes how to do this.
Your plugin can be integrated
into our system and will be accessed by the same mechanism as the built-in plugins.
This means that by adding 2-3 java classes (the Plugin
, the PluginLoader
and
optionally the PluginOutput
) that provide the uniform interface
for plugin handling, and setting your plugin configuration in the 2 configuration files
(one for the command line tool and the another one for the graphical sketcher/viewer) your
plugin will be run by the cxcalc command line tool, inserted
into the plugin set under the Tools
menu in
MarvinSketch and MarvinView, and
enabled for access it through CalculatorPlugin
API.
The following table shows the items needed for the different plugin uses:
CalculatorPlugin subclass | CalculatorPluginLoader subclass | CalculatorOutput class or subclass | plugins.properties | calc.properties | |
sketcher/viewer | |||||
cxcalc tool | |||||
java API |
Implementation step-by-step guide:
CalculatorPlugin subclass
The first step: go to the internet and download some code or write your own. The calculation code is assumed to work on one input molecule at a time, perform the calculation and then return various results of the calculation. The plugin class will first set the input molecule, then run the calculation and finally query the results, so it is a good idea to follow roughly the same implementation style in the calculation module: the more the calculation code follows this model, the easier your work will be when you write the plugin wrapper.
Then extend the abstract base plugin class chemaxon.marvin.plugin.CalculatorPlugin
.
Here is the list of methods that have to be implemented:
protected String getLicenseKey()
Implement this only if you want to sell your plugin and protect it with a license key.
The default implementation returns null
which means that the plugin is free:
no license key is required.
A license key is an 8-character string that is hard-coded in your plugin and sold to your clients
in the license key file chemaxon/licenses.txt
located in the
CLASSPATH
or in the .chemaxon/licenses.txt
(Unix) or
chemaxon/licenses.txt
(Windows) file under the user's home directory.
In the license key file, the plugin name with full package name is paired with the
license key: chemaxon.marvin.calculations.YourPlugin=56TYAD12The license key returned by this method will be checked against the license key loaded from the
licenses.txt
file. If they are equal then the plugin can be run without
restriction, otherwise it will run in demo mode: only one calculation can be performed without
restarting the application (e.g. cxcalc
will be run only for the first molecule of
the input SDF file).
The fact that this method has protected
accessibility level may result in a
security problem: someone might add a java class to the same package, instantiate your plugin
and try to fetch the license key:
CalculatorPlugin plugin = new YourPlugin(); System.out.println(plugin.getLicenseKey());There is a workaround for this: instead of simply returning your license key, implement this method in the following way:
protected final String getLicenseKey() { if (check()) { return "56TYAD12"; // return your license key here } else { return ""; // return the empty string which is the invalid key } }First note that the method is declared
final
to prevent the intruder from
subclassing your plugin and replacing your license key by overwriting this method.
Next note that you return your license key only if the check()
method
implemented in CalculatorPlugin
returns true
. Otherwise you
return the empty string which is the invalid key. Do not return null
which would mean that the plugin has no license key - if you return null
then
your plugin will be freely used without any license key.
The mysterious check()
method checks whether your plugin has been
validated. Validation is a simple process that checks whether the caller knows the
license key. This is done by a call to the validate(String license)
method.
Hence with the above implementation the intruder would simply print out the empty
string while a programmer who has bought your license key and would like to use your plugin
can do that by proving that he/she knows your license key:
CalculatorPlugin plugin = new YourPlugin(); System.out.println(plugin.getLicenseKey()); // prints the empty string plugin.validate("56TYAD12"); // supposing your license key is"56TYAD12" System.out.println(plugin.getLicenseKey()); // now prints the real license key
public void setParameters(Properties params) throws PluginException
This method sets the plugin specific parameters: the params
argument contains the
plugin parameters as long parameter name -> parameter value
pairs.
(The long parameter name
here is without the "--
" prefix: e.g. if you
have --type
as a command line parameter then it will be present with key
type
in this property table.) Your task is to
convert the parameter values from string to the required format and set the parameter
in the calculation module or store it in the plugin for later use. Throw a
PluginException
on any error (unexpected format, unexpected value). All possible
plugin parameters have a default value so a missing parameter should not cause any error: use
its default value instead.
public void checkMolecule(Molecule mol) throws PluginException
Checks the input molecule. Throws a PluginException
if the plugin calculation
result is not defined for the given molecule (e.g. molecule is a reaction molecule or a
molecule with R-groups). The exception message will be formed to an error message to the
user and the molecule will not be processed if a PluginException
is thrown.
Do nothing if the molecule is accepted. The default implementation accepts
all molecules (simply returns).
public void setMolecule(Molecule mol) throws PluginException
This sets the input molecule. Again, throw a PluginException
on any error.
public void run() throws PluginException
This method performs the calculation and stores the results. Include those tasks
that must be run once for each molecule and produce the calculation results in the end.
Throw a PluginException
on any error.
public Molecule getMolecule()
This simply returns the input molecule.
public Object[] getResultTypes()
This method returns the queried result types. For example, the charge
calculation may have three result types: sigma
, pi
and total
,
the logp
calculation may have two result types: increments
and
molecule
. The built-in plugins charge
, logp
and
pka
have the --type
command line parameter that specifies the required
result types: this method returns those that are specified in this parameter. However, it is
possible to return all available result types and not provide this choice.
public int getResultDomain(Object type)
This returns the domain that the calculation result for the specified result type refers to:
currently it can be ATOM
or MOLECULE
. For example, the
logPPlugin
returns ATOM
if key
is "increments"
and returns MOLECULE
if key
is "molecule"
.
public int getResultCount(Object type)
This returns the number of result items for the specified result type. For ATOM
result
domain this is usually the number of atoms in the molecule, for MOLECULE
domain this
is usually 1
.
public Object getResult(Object type, int index) throws PluginException
This returns the result for the specified result type and the specified result index: this index
must be at least 0
and less than the result count returned by
getResultCount(Object type)
for this result type. In our case the result is a number:
it must be wrapped into the derived class of java.lang.Number
corresponding to its
primitive type (e.g. double
must be wrapped into java.lang.Double
).
PluginException
can be thrown on any error.
public String getResultAsString(Object type, int index, Object result) throws PluginException
This returns a string representation of the result. The result type and index are also given:
in some cases the string representation may include these or depend on these as well. The
protected String format(double x)
can be used to double -> String
conversion with a given number of fractional digits. If you intend to use this formatting then
protected void setDoublePrecision(int precision)
has to be called once beforehand
to set the maximum number of decimal digits allowed in the fractional portion of a number.
PluginException
can be thrown on any error.
public String getResultAsRGB(Object type, int index, Object result) throws PluginException
Returns the color to be used when displaying the result. For example, this method is used when acidic pKa values are displayed
in red while basic pKa values are displayed in blue. The color is returned as a single int
(see the
java.awt.Color API for a
definition of encoding a color into a single int
). The default implementation returns 0
which means that the result will be displayed using the current foreground color.
PluginException
can be thrown on any error.
protected void standardize(Molecule mol)
This method is used to bring the molecule to a standardized form. Some calculations require a certain
standardization preprocess, such as aromatization or a common form of certain functional groups
(one prescribed tautomer or mezomer form). The current implementation performs only aromatization
and the nitro group conversion [O-]-[N+]=O >> O=N=O
. If any other transformation is
needed or no such transformation is necessary then you must implement this method. Be careful with
transformations that change the atom set of the molecule since these change the atom indices as
well: if the result domain is ATOM
then after querying the results with
getResult(Object key, int index)
and
getResultAsString(Object key, int index, Object result)
the program will output the returned result for the specified atom index
in the original molecule and not in the transformed one.
If the standardization procedure changes the atom indices then the index given in these result
query methods must be transformed to the corresponding atom index in the transformed molecule
and the result for that atom index must be returned.
Remark: This local standardization will be replaced by a general standardizer module.
public String getOutputClassName()
This method is called by chemaxon.marvin.Calculator
to get the class name of the
table form output provider class corresponding to your plugin. The default implementation
returns "chemaxon.marvin.plugin.CalculatorOutput"
which is the class name of
the default table form output provider (outputs the molecule ID and the result strings for each
required result type). Implement this method by returning to your specific output provider
class name only if it is different from chemaxon.marvin.plugin.CalculatorOutput
.
CalculatorPluginLoader subclass
Extend the abstract base plugin loader class
chemaxon.marvin.plugin.CalculatorPluginLoader
.
Here is the list of methods that have to be implemented:
public Component getParameterPanel()
This method returns the parameter panel as a java.awt.Component
object.
This component will be placed on the general options pane when the user invokes the
plugin parameter setting through the corresponding Tools/Options
submenu item.
public Properties getParameters()
This method returns the parameters that has been set on the parameter panel (or the default
values if the parameter setting has not been invoked). This object will be set in the plugin
when the calculation is run by the user. The parameter keys have to match the parameter long names
configured in the cxcalc
configuration because
the chemaxon.marvin.Calculator
class sets the parameter long names as property keys.
public boolean verifyParameters()
This method verifies the parameter settings on the parameter panel, called when the user presses
the "OK" button on the option pane: the option pane is hidden and parameters are accepted only if
this method returns true
, otherwise the option pane remains shown to let the user
correct the parameter settings. It is recommended to show an error message dialog before returning
false
to let the user know the parameter setting error.
public void updateParameters()
This method is called when the user presses the "OK" button on the option pane and the parameter
settings are accepted by verifyParameters()
. The Properties
object or any
inner structure storing the user settings can be filled with the parameter panel data. In theory
even the parameter panel component object itself can store this data, but an auxiliary structure
may be more efficient: the Properties
object is queried each time the plugin is
run and it is better to perform data conversion from the parameter panel only once per parameter
setting. Another reason for storing parameter data outside of the panel is that the user could change setting then press the
"CANCEL" button in which case we would want know the previous parameter settings!
public void reloadParameters()
This method is called when the user presses the "CANCEL" button on the option pane. In this case, the panel should
be reset to the previous settings.. This can be
done from the Properties
object or any other inner structure used to store the
parameter settings. This is the reason why you should not use the pure GUI component to store the
parameters, so that previous settings can be recalled.
protected String getPluginClassName()
This method is called by the dynamic plugin loading mechanism to get the plugin class name.
The default implementation simply strips off the last 6 characters (the "Loader" ending) from
the loader class name. All built-in plugins have both their plugin class and their loader class
in the chemaxon.marvin.calculations
package and follow a simple naming convention:
plugin class names have a base name ending with "Plugin", loader class names have the same base
name ending with "PluginLoader" (e.g. chemaxon.marvin.calculations.ChargePlugin
and
chemaxon.marvin.calculations.ChargePluginLoader
). If your plugin and loader class
naming follow this convention then you need not implement this method, otherwise return the full
plugin class name here.
CalculatorPluginOutput subclass
This is optional: the default table output is implemented in
chemaxon.marvin.plugin.CalculatorOutput
. This implementation outputs
the molecule ID and the results for all required result types in a table row. To provide a
different output table form you should subclass
chemaxon.marvin.plugin.CalculatorOutput
and return the full class name
of your output class in public String getOutputClassName()
implemented in your
plugin class. You should implement the following methods in your output class:
public String getHeader()
Returns the table header string.
public String getResult(Molecule target)
Returns the result table row for the given target molecule.
protected
member variables of
chemaxon.marvin.plugin.CalculatorOutput
:
CalculatorPlugin plugin
is your plugin object
Properties params
stores your plugin parameters
String separator
is the separator string between result items
Calculator.properties
configuration file
See the Configuration File section of the Calculator user manual.
plugins.properties
configuration file
See the Configuration section of this manual.
Here are some examples that illustrate the usage of the plugin API.
Note the validate(<license key>)
is called right after the plugin
instantiation: this is important because otherwise you can only use the plugin
without license in demo mode.
// fill parameters Properties params = new Properties(); params.put("precision", "3"); params.put("type", "total,sigma"); // create plugin ChargePlugin plugin = new ChargePlugin(); plugin.validate(<charge license>); // set plugin parameters plugin.setParameters(params); // set target molecule MolInputStream mis = new MolInputStream(new ByteArrayInputStream("Clc1cc(Cl)c(Cl)cc1".getBytes())); MolImporter mi = new MolImporter(mis); plugin.setMolecule(mi.read()); // run the calculation plugin.run(); // get the calculation results Object[] types = plugin.getResultTypes(); // now this is {"total", "sigma"} for (int i=0; i < types.length; ++i) { Object type = types[i]; System.out.println(); System.out.println(type + " charge: "); System.out.println("atom\tcharge"); int count = plugin.getResultCount(type); // now this is the atom count for (int j=0; j < count; ++j) { Object result = plugin.getResult(type, j); // now this is the charge value on atom j String rtext = plugin.getResultAsString(type, j, result); // string representation System.out.println((j+1)+"\t"+rtext); } }
// fill parameters Properties params = new Properties(); params.put("type", "logP,increments"); // create plugin logPPlugin plugin = new logPPlugin(); plugin.validate(<logP license>); // set plugin parameters plugin.setParameters(params); // set MolImporter MolInputStream mis = new MolInputStream(new FileInputStream("target.sdf")); MolImporter mi = new MolImporter(mis); // for each input molecule run the calculation and display the results System.out.println("molecule\tlogP\tincrements"); Molecule target = null; while ((target = mi.read()) != null) { // set the input molecule plugin.setMolecule(target); // run the calculation plugin.run(); // get the overal logP value double logp = ((Double)plugin.getResult("logP", 0)).doubleValue(); // get the incremental values int count = target.getAtomCount(); String[] increments = new String[count]; for (int i=0; i < count; ++i) { Object result = plugin.getResult("increments", i); increments[i] = plugin.getResultAsString("increments", i, result); } // display the results StringBuffer s = new StringBuffer(); s.append(target.toFormat("smiles")+"\t"+logp+"\t"); for (int i=0; i < count; ++i) { if (i > 0) { s.append(";"); } s.append(increments[i]); } System.out.println(new String(s)); }