CALO Express (CE) Plug-In Architecture

CE provides a plug-in architecture to easily insert code for accessing harvested data, implementing new learning algorithms, adding user interfaces, and defining application logic. While C# and Java plug-ins can access the harvested data and implement new algorithms, only C# plug-ins can insert user interfaces and define application logic.

FeatureC#Java
Harvested DataYesYes
AlgorithmsYesYes
Application LogicYesNo
User InterfaceYesNo

Support Features

CE supports the following features for plug-ins:

CE Plug-In Developer Tutorial

Developing a CE plug-in is a two step process: Initially, you build the plug-in either in C# or Java. Then, you build plug-in dependency files by bundling files required by the Plug-ins at runtime. Plug-in dependency files include related .dll files for C# plug-ins, .jar files for Java Plug-ins, or data resource files needed by either C# or Java Plug-ins.

Prerequisites

How to create a C# plug-in

1.       Install CALO Express.

2.       Create the Visual Studio project:

a.       Open Microsoft Visual Studio.

b.      Create a "Visual C# Class Library" project.

c.       In the "Application" tab of the project properties, change the "Assembly name" so that it ends in "-CEDotNet" (e.g. "HelloWorld-CEDotNet").

3.       Create the Ant build script:

a.       Copy the build.xml script from the HelloWord example plug-in to your project directory.

b.      In build.xml, set <project name>, plug-in.name, and vStudio.solution appropriately.

c.    In build.xml, set the build-plug-in-impl target so it only depends on "build-dotNet". <target name="build-plug-in-impl" depends="build-dotNet"/>

4.    Choose the CE process or processes where your plug-in will run. Run UI and learning algorithms in CaloManager. Run harvesting extensions in CaloHarvester. Run Outlook-specific code in CaloAddIn.

5.       Use a Windows Command prompt to subst the L: drive to point to the location of your CALO Express installation.
subst L: "c:\Program Files\SRI\CALO Express".

6.       In your Visual Studio project, add references to CaloCommon.dll and any other CE dlls that your plug-in uses. Use relative links through the L: drive, like L:\CaloCommon.dll. The L: drive works for building and debugging your project. When your plug-in is running in a deployed system, L: will not exist, but CE will guarantee these dlls are in the path.

7.       Create your plug-in entry points by making classes that implement the CaloCommon.Service.IModule interface. This is a simple interface with one method, Startup(). It acts like "Main()" for an application. Be sure these classes are marked "public."

8.       Make a text file with a list of your plug-in entry points. For each of the CE processes where you want your code to run, in Visual Studio add a new text file in the root directory of your project. Name the file "modules-{processName}.txt" (e.g. modules-CaloManager.txt. file). In the VS Properties for the file, change the "Build Action" to "Embedded Resource". This file is a list of the fully qualified classnames of all your plug-in entry point classes, one classname per line.

9.       Access CALO Express services. Browse the CE source code or API documentation to find the appropriate service interface that derives from IService. Then use ServiceManager.GetService() to get an instance.

10.   Install your own services. Create a public interface that derives from IService. Create a public class that implements your service interface as well as the IModule interface. Add a line to modules-*.txt file with the classname of the implementing class, similar to what you did with your plug-in entry points.

11.   Add logging to your plug-in. You can use CaloCommon.Utilities.Logging.Log to easily make Log.Info/Warn/Error() calls to the common CALO Express log file. But, if your plug-in will generate a significant number of log messages, you may want to use a custom log file for your plug-in. If so, follow these steps:

a.       Create a public interface for your logging class, see HelloLog.IHelloLog.cs in the HelloWorld sample plug-in. Create a logging interface appropriate to your application.

b.      Create your logging implementation class, see HelloLog.HelloLogImpl.cs. Derive from the AbstractFileLog class.

c.       Add your logging implementation class to the modules-*.txt file for your plug-in.

d.      Copy the file log4net.config from the HelloWorld plug-in into your project directory. Add it to your Visual Studio project. In the VS Properties for the file, change the "Build Action" to "Embedded Resource."

e.      Edit your log4net.config file and change <appender name>, <logger name>, and <appender-ref ref> to match the name of your logging implementation class. If desired, set the max size and number of rolling log backup files with <maximumFileSize> and <maxSizeRollBackups>.

f.        Add logging statements to your code like this:
IHelloLog hlog = ServiceManager.GetService(typeof(IHelloLog)) as IHelloLog;
hlog.WriteLine("This is logging.");

g.       At runtime, log output will be in:
C:\Documents and Settings\{USERNAME}\caloExpressPersistence\ver-XX\{LOGPERSISTENCEMODULE}\{LOGBASENAME}

12.   From the command line, run "ant build-plug-in" and your plug-in will be created in "dest\Release" under your project directory (e.g. HelloWorld-CEDotNet.dll).

13.   Copy your plug-in to your CE Plug-ins directory, usually "C:\Program Files\SRI\CALO Express\Plug-ins," and then log out and log back in to Windows. CE will now run the new plug-in.

Hint for advanced developers:

How to create a Java plug-in

Since CE Java plug-ins are generally paired with a C# plug-in, this section assumes that you have completed "How to make a C# Plug-in".

1.       Subst L: drive as described in "How to make a C# Plug-in"
subst L: "c:\Program Files\SRI\CALO Express"
For development, use relative links like L:\expressCommon.jar to build and debug your project. At deployment time, CE will guarantee that these jar files are in your CLASSPATH.

2.       Make a "Java" subdirectory under your project directory with further subdirectories "src" and "lib" for your source code and any dependent jar files, respectively. If you have dependent jar files, you will need to include them in a Plug-in Dependencies File as well (see next section).

3.       Use the Java development environment of your choice (e.g., IntelliJ, Eclipse, or a text editor with javac and make) and create a Java project:

a.       Place source files under the Java/src directory.

b.      Include a reference to L:\Java\experssCommon.jar for access to CE Java services.

c.       Look at the available third-party jar files in L:\Java to see if your code uses these libraries (e.g., log4j, Xerces). If so, make sure your code works with those exact versions and make references to the L:\Java\*.jar files in your project. CE does not yet provide a mechanism to handle version conflicts between third-party jar files.

4.       Create a Java plug-in entry point. A public class with a "public static void startup()" method and no constructor.

5.       Modify the Ant build.xml script:

a.       Set the java.dir, java.src.dir and java.lib.dir properties appropriately.

b.      Set CaloJava.modules to be the classname of you plug-in entry point.

c.       In build.xml, set the build-plug-in-impl target so it includes "build-java".
<target name="build-plug-in-impl" depends="build-dotNet,build-java"/>

6.    Expose an XmlRpcService from your Java code:

a.       Create a public class with some public methods and no constructor to implement the service interface. See HelloJavaService.java. The interface must include only types serializable via XML-RPC, see http://www.xml-rpc.net for XML-RPC documentation.

b.      In your plug-in startup code, call XmlRpcUtilities.startXmlRpcService() with the classname of your service implementing class. See HelloJavaStartup.java.

7.       Call the XmlRpcService from your C# plug-in:

a.       Add L:\ CookComputing.XmlRpcV2.dll to the references in your Visual Studio project.

b.      Create an interface that extends IXmlRpcProxy and exactly matches the service class exposed from your Java code. See IHelloJavaXmlRpc.cs.

c.       Create an interface that extends IService and provides the interface you want to share throughout your code (see IHelloJavaProxy.cs). This can use high-level types not allowed in the IXmlRpcProxy interface.

d.      Create a proxy class that implements both your IService interface and IModule. Implement the IService interface with calls to the IXmlRpcProxy interface, serializing and deserializing types and including error handling as necessary (see HelloJavaProxy.cs).

e.      Add a line to modules-*.txt file with the classname of the proxy class, similar to what you did with your C# plug-in entry points.

8.       To access CE preferences, use com.sri.caloExpress.common.PreferencesSingleton to get a Preferences object and call the methods there.

9.       To use CE's log4j logging:

a.       Add "L:\Java\log4j-1.2.14.jar" to your Java project.

b.      Add a Logger.getLogger() call at the top of a Java file.
static Logger log = Logger.getLogger(HelloJavaStartup.class);

c.       Call log.info(), log.warn(), log.error() as appropriate.

d.      At runtime, log output will be in:
C:\Documents and Settings\{USERNAME}\caloExpressPersistence\ver-XX\caloLog\caloLog.txt

10.   From the command line, run "ant build-plug-in" and your plug-in will be created in "dest\Release" under your project directory (e.g., HelloWorld-CEJava.jar).

How to create a Plug-in dependencies File

This section assumes you have completed "How to make a C# plug-in".

1.       Subst L: drive as described in "How to make a C# plug-in"

2.       Gather all the dependent files that your C# and/or Java plug-ins need to run:

a.       For C# plug-ins, there are often dependent .dll files. Place them in the "lib" directory of your project.

b.      For Java plug-ins, there are often dependent .jar files. Place them in the "Java\lib" directory of your project.

c.       For both C# and Java plug-ins, there are often other dependent data files. Place them in the "data" directory of your project.

3.       Modify the Ant build.xml script:

a.       For the first directory of dependent files, add a <zipfileset id="dependencies.zipfileset.1"> line to the init target with the location of the dependent files in your project and the target location at deployment time. See build.xml in the HelloWorld sample plug-in source.

b.      If there are multiple directories of dependent files, add <zipfileset id="dependencies.zipfileset.2">, <zipfileset id="dependencies.zipfileset.3">...lines to the init target.

c.       In build.xml, set the build-plugin-impl target so it includes "build-dependencies-X" where X is the number of directories of dependent files (e.g. <target name="build-plugin-impl" depends="build-dotNet,build-java,build-dependencies-1"/>).

4.       From the command line, run "ant build-plug-in" and your Plug-in Dependency File will be created in "dest\Release" under your project directory (e.g. HelloWorld-CEDependencies.zip).

Sample Code

The following C# file, HelloWorldStartup.cs, from the HelloWorld example plug-in demonstrates:

1.       The IModule plug-in entry point

2.       Registering an action on the CALO Button - the "Hello World" action

3.       Writing to the (read-write) CE persistence directory - record the number of times the plug-in has been run

4.       Log4net logging from a plug-in - to both the global CE log file and a plug-in-specific log file

using System;
using System.Collections.Generic;
using System.Text;
using CaloCommon.Service;
using CaloManager.Action;
using CaloItem.Item;
using CaloManager.SearchBox;
using CaloCommon.Utilities.Logging;
using System.IO;
using CaloCommon.CaloSystem;
using HelloWorld.Logging;

namespace HelloWorld
{
    public class HelloWorldStartup: IModule
    {
        /// 
        /// The main plug-in entry point. Register actions and perform any other initialization.
        /// 
        public void Startup()
        {
            int runCount = UpdateRunCount();
            // Register a user-visible action for the CaloButtonContextClick ActionContext.
            IActionService actionService = ServiceManager.GetService(typeof(IActionService)) as IActionService;
            actionService.RegisterAction(typeof(IItem),
                
	      // For 1.6.0.3.
              // typeof(CaloButtonActionContext),

              // In 1.6.1.X and later use CaloButtonContextClickActionContext.
              typeof(CaloButtonContextClickActionContext),

              new HelloWorldAction(runCount));

            // Logging to common log file for all of CE.
            Log.Info("Hello, World logging to the common CALO Express log file.");

            // Logging to plug-in-specific log file.
            IHelloLog hlog = ServiceManager.GetService(typeof(IHelloLog)) as IHelloLog;

            hlog.WriteDivider();

            hlog.WriteLine("Welcome to the HelloWorld custom log file.");

        }

        /// 
        /// Demonstrate use of CE persistence directory. Here is where plug-ins can store read-write state
        /// that is persistent over multiple runs.
        /// 

        private int UpdateRunCount()
        {
            ICaloSystem system = ServiceManager.GetService(typeof(ICaloSystem)) as ICaloSystem;
            string runCountFile = Path.Combine(system.GetPersistenceDir(HelloConstants.PERSISTENCE_MODULE), HelloConstants.RUN_COUNT_FILE);

            int runCount;
            if (!File.Exists(runCountFile))
            {
                runCount = 0;
            }
            else
            {
                StreamReader reader = new StreamReader(runCountFile);
                runCount = int.Parse(reader.ReadToEnd());
                reader.Close();
            }

            StreamWriter writer = new StreamWriter(runCountFile);
            writer.Write(++runCount);
            writer.Close();

            return runCount;
        }
    }
}

 

 

The following C# example demonstrates:

1.       Implementing a CE action

2.       Using the ServiceManager to access CE services

3.       ISearchEngine - get count of items in search index

4.       ICaloSystem - get data directory

5.       IPreferences - get user's favorite smiley face string

6.       Reading a (read-only) data file bundled with the plug-in via a Plug-in Dependency file

7.       Calling a Java XML-RPC service via a C# proxy class - IHelloJavaProxy

 

using System;
using System.Collections.Generic;
using System.Text;
using CaloManager.Action;
using CaloItem.Item;
using CaloCommon.Utilities;
using CaloCommon.CaloSystem;
using CaloCommon.Service;
using System.IO;
using CaloItem.Search;

namespace HelloWorld
{
    public class HelloWorldAction: AbstractAction
    {
        // In CE 1.6.1.X, this is available in CaloSystemConstants, but include it explicitly here
        // for compatibility with CE 1.6.0.3.

        const string PREFERENCES_MODULE_NAME = "preferences";
        int runCount;
        public HelloWorldAction(int runCount)
            :base("Hello World","Run the Hello World action of the example plug-in.",false)
        {
            this.runCount = runCount;
        }

        /// 
        /// Demonstrate access to read-only resource files embedded in the Plug-in Dependency File.
        /// 

        private string ReadDataFile()
        {
            ICaloSystem system = ServiceManager.GetService(typeof(ICaloSystem)) as ICaloSystem;
            string dataFile = Path.Combine(system.DataDir, HelloConstants.DATA_FILE);

            try
            {
                StreamReader reader = new StreamReader(dataFile);
                string ret = reader.ReadToEnd();
                reader.Close();
                return ret;
            }

            catch (Exception)
            {
                return "Unable to read data file " + dataFile;
            }
        }

        /// 
        /// XmlRpc call to code in the Java Plug-in.
        /// 

        private string CallHelloJava()
        {
            IHelloJavaProxy proxy = ServiceManager.GetService(typeof(IHelloJavaProxy)) as IHelloJavaProxy;
            return proxy.HelloJava();
        }

        /// 
        /// Entry point to perform the action.
        /// 

        public override void Perform(ActionContext context, IList items)
        {
            // ICaloSystem has a variety of useful file system locations and values.
            // Guaranteed to have good values for both a development and deployment environment.
            ICaloSystem system = ServiceManager.GetService(typeof(ICaloSystem)) as ICaloSystem;

            // Example of getting a preference.  (And setting the default pref value if this is
            // the first time the pref is used.)
            IPreferences prefs = ServiceManager.GetService(typeof(IPreferences)) as IPreferences;
            string smiley = prefs.GetStringValueCreate(HelloConstants.SMILEY_PREF, HelloConstants.SMILEY_DEFAULT);

            // You don't need these lines to use preferences, only for displaying system information.
            string prefsDir = system.GetPersistenceDir(PREFERENCES_MODULE_NAME);
            string smileyPrefLocation = Path.Combine(prefsDir, HelloConstants.SMILEY_PREF + ".pref");

            // Use ISearchEngine to access the store of harvested items.
            ISearchEngine searcher = ServiceManager.GetService(typeof(ISearchEngine)) as ISearchEngine;
            int itemsInIndex = searcher.ItemsInIndex();

            // Format the output to display in dialog.
            StringBuilder builder = new StringBuilder();
            builder.Append("Hello, World!\n\n");
            builder.Append("This plug-in has been run " + runCount + " times.\n\n");
            builder.Append("This is my favorite smiley face " + smiley + "\n");
            builder.Append("You can change this preference at " + smileyPrefLocation + "\n\n");
            builder.Append("Contents of " + HelloConstants.DATA_FILE + " from Plug-in Dependency File:\n");
            builder.Append(ReadDataFile() + "\n\n");
            builder.Append("The search index contains " + itemsInIndex + " items.\n\n");
            builder.Append("XmlRpc call to the " + HelloConstants.HELLO_JAVA_SERVICE_NAME +
                " service running as a Java Plug-in in the Java process gives:\n");
            builder.Append("------------------------------\n");
            builder.Append(CallHelloJava());
            builder.Append("\n------------------------------");

            // Simple MessageBox showDialog.  Use custom C# forms for more complex UI.
            CaloDialog.ShowInfo(builder.ToString());
        }
    }
}

 

The following example demonstrates:

1.       The startup() plug-in entry point

2.       Starting a Java XML-RPC service - HelloJavaService

3.       Log4j logging from a plug-in - to the common log file for all CE java code

 

package com.sri.calo.hellojava;

import org.apache.log4j.Logger;
import com.sri.calo.caloCommon.XmlRpcUtilities;

public class HelloJavaStartup {
    public static Logger log = Logger.getLogger(HelloJavaStartup.class);

    private static String SERVICE_NAME = "HelloJava";

    public static void startup() {
        // Start the XmlRpc service implemented by the class HelloJavaService.
        int assignedPort = XmlRpcUtilities.startXmlRpcService(SERVICE_NAME,HelloJavaService.class);

        // Log to common log file for all CE java code.
        if (assignedPort != XmlRpcUtilities.INVALID_PORT) {
            log.info("Started " + SERVICE_NAME + " on port " + assignedPort);
        }
        else {
            log.error("Failed to start " + SERVICE_NAME);
        }
    }
}

 

The following Java file, HelloJavaService.java, from the HelloWorld example plug-in demonstrates:

1.       Implementing a Java XML-RPC service - HelloJavaService with public method helloJava()

2.       The Preferences class for preferences common to C# and Java code - get favorite smiley face

 

package com.sri.calo.hellojava;

import java.io.StringWriter;
import org.apache.log4j.Logger;
import com.sri.calo.caloCommon.PreferencesSingleton;
import com.sri.calo.caloCommon.CaloConstants;
import com.sri.calo.caloCommon.CaloSystem;
import com.sri.calo.caloCommon.generatedCode.GeneratedConstants;

public class HelloJavaService {
    public static Logger log = Logger.getLogger(HelloJavaService.class);

    private final static String SMILEY_PREF = "HelloWorldSmiley";

    public String helloJava() {
        // Log to common log file for all CE java code.
        log.info("HelloJavaService.helloJava() called.");

        // Demonstrate getting a preference from Java code.
        String smiley = PreferencesSingleton.singleton().getStringValue(SMILEY_PREF);

        // The CE framework exposes several CE-specific folders as Java system properties.
        StringWriter ret = new StringWriter();
        ret.append("Hello from Java process!\n");
        ret.append("JavaDir=" + System.getProperty(GeneratedConstants.CALO_JAVA_DIR_PROPERTY) + "\n");
        ret.append("VersionedPersistenceRoot=" + CaloSystem.getVersionedPersistenceRoot() + "\n");
        ret.append("SearchIndexDir=" + CaloSystem.getSearchIndexDir() + "\n");
        ret.append("Plug-insDir=" + System.getProperty(GeneratedConstants.CALO_PLUG-INS_DIR_PROPERTY) + "\n");
        ret.append("My favorite smiley is: " + smiley);
        return ret.toString();
    }
}

 

CALO Express Plug-Ins

The CE plug-in architecture was used to integrate the following PAL components with CALO Express.