Interactive Plugin Loading

From PCGen Wiki
Jump to: navigation, search

Purpose of this document

This document defines the replacement interfaces/classes required to update the method used to load "Interactive Plugins".

Context/Background

PCGen currently has - at a high level - 2 forms of plugins.

  • "Simple plugins" (mainly the LST tokens, but also including Primitives, Qualifiers, BONUSes, Export Tokens, Formula elements)
  • "Interactive Plugins" (plugins that can have a user interface, interact, save information, etc - things like GMGen)

Loading and Using Plugins

The "Simple Plugins" are currently loaded by a process and interface written by Connor, and is related to event handling in GUI2. The simple token loading is independent and not changing for this exercise (Connor already wrote a different load system, so we aren't using "old" code)

While the "Interactive Plugins" are loaded in the GUI2 style, there are still multiple issues with the current design:

  • We need to replace the code that defines the plugins and allows them to communicate (CODE-1849)
  • The plugins currently use static methods within the PCGen core (GMGen core, really) and thus are not easily tested or separable from the actual classes used. This should be transitioned to a more friendly interface/dependency-injection model.

Order of Operations

Note this is implemented already, not in the scope of this project

The order of operations for "Interactive Plugin" load is:

  • start(PCGMH)
  • add to chain of responsibility (see below)
  • send message to the "dispatcher" (which passes it through the chain of responsibility) indicating the new interactive plugin has been added

Note: during start(PCGMH) a plugin MAY send messages down the chain of responsibility. It should do so KNOWING it will not receive responses because it is not yet a member of the chain of responsibility.

Let's call the chain of responsibility class "ChainOfResponsibility" for now. Consider it called as such:

 (field) ChainOfResponsibility cor = new ChainOfResponsibility();
 
 ...
 PCGenMessageHandler dispatcher = cor.getMessageDispatcher();
 for (InteractivePlugin plugin : treeSetOfPrioritizedPlugins)
 {
 plugin.start(dispatcher);
 cor.addMember(plugin);
 }
 ...

cor.addMember should do 2 things:

  • Add the plugin to the chain of responsibility.
  • Notify the chain that a new member has been added by sending a ComponentAddedMessage(plugin) to the postbox.

cor.removeMember should do the opposite of those 2 actions (in reverse order)

Note: this consciously keeps the the knowledge of the concept of priority OUT of the Chain of Responsibility so that other exceptions could be made in the future, and keeps the COR only worried about sending messages, not about a concept of priority that is primarily related to load order, not responsibility order. (see below for more about priority)

Required Interfaces and Classes

The following are the interfaces and classes needed to implement this new "Interactive Plugin" loading system:

An interface for handling messages

For purposes of this discussion: "PCGenMessageHandler"

This interface has:

  • A void "handleMessage" method that receives one parameter - a PCGenMessage (see the section on communication)

There are 2 uses of this interface. There is a "primary message dispatcher" provided to the plugins and each plugin must also implement this method (see below)

An interface to define an "Interactive Plugin"

For purposes of this discussion, we call this interface "InteractivePlugin". All "main classes" of "Interactive Plugins" will implement this interface.

These must have the following characteristics:

  • InteractivePlugin extends PCGenMessageHandler
  • An integer Priority (used to sort which items are loaded first, lower priority is first)
  • A (String) name for the plugin, fetched with a zero argument method
  • A (String) indicating the identifier for the source of events [is not (just) the plugin name], fetched with a zero argument method
  • A one argument start(PCGenMessageHandler) method (sent to the Plugin when it is started, intended to perform startup & allocation.)
  • A zero argument stop() method (sent to the Plugin when it is stopped, intended to deallocate any resources)
  • A Method to identify the data directory in which plugin information should be stored, fetched with a zero argument method that returns a String

Note regarding priority: Either InteractivePlugin must extend Comparable<InteractivePlugin> or a Comparator<InteractivePlugin> capable of handling the priority must be provided as part of the implementation. (The net is you need to be able to use InteractivePlugin as a key in a TreeMap)

Note for documentation: Plugins should NOT attempt to send a message across the bus indicating they were loaded. That is a default function of the Plugin loading system (done by the chain of responsibility), and it is performed AFTER the start() method completes. (Note: It is intended that start should not have to concern itself with thread safety and any message being received while start is running. The notification to other subsystems is only made after start() has been called)


A Message definition

For purposes of this discussion we will call this event: "PCGenMessage". This class (or interface) needs to be defined.

Also, all PCGenMessages will behave as good events (PCGenMessage may extend java.util.EventObject) and thus the source of the message is available. (This includes allowing the plugins a chance to check the source of the PCGenMessage).

PCGenMessages must also be consumable (see below).

A Class for Sending messages to all Plugins

Develop a class that implements a Chain of Responsibility. The members of the chain of responsibility are the prioritized list of plugins. (Note: the prioritization is done OUTSIDE the chain of responsibility by a TreeSet. The Chain of Responsibility should assume items are loaded in the appropriate order)

Plugins can register to be a member of the chain of responsibility (This is done during Plugin Load, you do not need to worry about this step, but must provide appropriate methods to add a new member (as in the example code above), etc.). To register, the class must be a PCGenMessageHandler.

Ensure that adding/removing items from the chain of responsibility is thread safe. (EventListenerList can be leveraged to provide this feature, if desired). Since the addition/removal takes place in a single plugin manager, this class should not need static methods or fields, but should simply be a normal Java class instance (no singleton required, either)

The ChainOfResponsibility "has a" PCGenMessageHandler field that is the "primary message dispatcher" ... that's fetched in the getMessageDispatcher() method in the example code above. (A relationship of "is a" or the dispatcher "knows a" chain of responsibility ("has a" but was passed into a constructor) can result in difficulty avoiding cycles in the design.

Note: I would assert from the "a class should only do one thing" principle that the chain of responsibility should not be the message dispatcher, but should have the method to get the dispatcher.


A Class for Receiving messages from Plugins

The Chain of Responsibility has a primary message dispatcher. This dispatcher implements PCGenMessageHandler. This dispatcher is intended to be the implementation of PCGenMessageHandler that is passed to each Plugin's start() method, and then when it receives a message from a plugin, it triggers and/or performs the increment across the chain of responsibility. (See distributing message, below)

As shown above, a method must be available for the Plugin Loader to get this primary message handler (in order to provide it to the plugins) [presumably this primary message dispatcher is "owned" by the system that contains the list of objects which are part of the chain of responsibility... meaning the chain of responsibility class "has a" primary message dispatcher - a private embedded class is probably necessary to ensure acyclic class file dependencies; unless you want to define a single use interface]

Distributing messages

When a message is to be sent along the chain, plugins are given a chance to process the message in the order in which the plugins registered (thus their priority), with one exception. The source object of the message is always given the lowest responsibility (always given back their own message only at the end of the chain of responsibility).

The messages sent along the chain of responsibility are consumable. All PCGenMessages may be, but are not required to be consumable (no rejection should be attempted by a plugin, no exceptions thrown, etc).

If a message is consumed, it is not distributed to other members of the chain of responsibility. (Therefore the PCGenMessage will need a method to set/get the consumed state).

It is intended that the ability to get the consumption state of a message is available to the object that originally passed the message to the primary message dispatcher (the easiest way to do this is to simply have the messages have a getState or some such on the PCGenMessage class/interface)

Some messages may not be consumable, if they are intended as system-wide messages. Component Addition and Removal are examples of messages that should not allow consumption. It is advised that a friendly way of indicating non-compliance with consumption (return false rather than throw an exception) be used, but actual implementation is left to the developer.

Note: No judgement is made as to whether this distributor is the same class as the sender and/or receiver. (That is implementation specific, and up to the programmer to establish the best architecture - just avoid cyclic dependencies :D )

Note on use: The "core"/"UI" can trigger distribution (when a PC is closed or opened for example), or the plugins can generate messages on certain events. So anyone with access to the main dispatcher can send a message (it doesn't have to be a member of the chain of responsibility to send a message). It generally is initiated by some user action, so it's generally a UI, but that is not guaranteed. Note: it is LEGAL for a plugin to send messages during it's start(dispatcher) method.

The main dispatcher takes any message it receives in its handleMessage method and distributes it down the chain of responsibility (checking for source, consumption, et al).

Messages to be implemented

In addition, the following message types need to be defined:

  • When File//New is selected
  • When File//Open is selected
  • When File//Save is selected
  • When Edit//Cut is selected
  • When Edit//Copy is selected
  • When Edit//Paste is selected
  • When a request to save the PC is made (e.g. a plugin can trigger this)
  • When a save of the PC has been made (to let others know to save "local" data)
  • When a request to OPEN the PC is made (e.g. a plugin can trigger this)
  • When a OPEN of the PC has been completed (to let others know to load "local" data)
  • When a PC is closed
  • To request a "virtual" "file opened" message for all PCs currently open (used on initial plugin load)
  • When Combat has been initiated
  • When a Combatant has been updated
  • To send a set of Initiative values between plugins
  • When to pause refreshing of the User Interface
  • When to resume refreshing of the user interface
  • When focus (or other state change) occurs
  • When a Plugin wants to add a menu item to the Tools Menu
  • When a Plugin wants to add a Panel to the Preferences panels
  • When a Plugin wants to add a Tab to GMGen
  • When PCGen is closed


General comments for documentation

Passing a message to the primary message dispatcher *transfers ownership of the message*. It is NOT intended that the message be reusable, and the PCGen core should make no guarantee as such (even if it may be possible in certain circumstances, no guarantee should be made)