bea.com | products | dev2dev | support | askBEA
 Download Docs   Site Map   Glossary 
Search

Programming BPM Plug-Ins

 Previous Next Contents Index View as PDF  

Defining the Plug-In Session EJB

This section explains how to define the plug-in session EJB. It includes the following topics:

 


Overview

To define the plug-in session EJB, you must implement the three predefined interfaces described in the following table.

Table 3-1 Interfaces Required by the Plug-In Session EJB  

Name

Interface

Description

Session EJB

javax.ejb.SessionBean

Interface that must be implemented by all session EJBs.

Plug-In Home Interface

com.bea.wlpi.server.plugin.PluginHome

Extension of the javax.ejb.EJBHome interface; it defines the home interface for all plug-ins.

Plug-In Remote Interface

com.bea.wlpi.server.plugin.Plugin

Extension of the javax.ejb.EJBObject interface; it defines the remote interface for all plug-ins.


 

The following sections describe each interface and the methods that you must implement when defining the plug-in session EJB. Excerpts from the plug-in sample are included.

 


Session EJB Interface

By definition, a session EJB must implement the javax.ejb.SessionBean and its methods.

Note: The contents of the SessionBean interface methods may be empty, or they may simply return a message to the log; but they must be implemented.

The following table lists the session EJB methods that you must implement.

Table 3-2 Session EJB Methods  

Method

Description

public void ejbActivate() throws javax.ejb.EJBException, java.rmi.RemoteException

Activates instance.

public void ejbPassivate() throws javax.ejb.EJBException, java.rmi.RemoteException

Passivates instance.

public void ejbRemove() throws javax.ejb.EJBException, java.rmi.RemoteException

Removes instance.

public void setSessionContext(javax.ejb.SessionContext ctx) throws javax.ejb.EJBException, java.rmi.RemoteException

Sets associated session context.

The method parameter is defined as follows:

ctx:
javax.ejb.SessionContext object that specifies the session context.


 

For more information about these methods, see the javax.ejb.SessionBean Javadoc.

The following code listing is an excerpt from the plug-in sample that shows how to implement the javax.ejb.SessionBean interface and its methods. This excerpt is taken from the SamplePluginBean.java file in the SAMPLES_HOME/integration/samples/bpm_api/plugin/src/com/bea/wlpi/tour/po/plugin directory. Notable lines of code are shown in bold.

Listing 3-1 Implementing the Session EJB Interface

package com.bea.wlpi.tour.po.plugin;

import com.bea.wlpi.common.VersionInfo;
import com.bea.wlpi.common.plugin.*;
import com.bea.wlpi.common.plugin.PluginData;
import com.bea.wlpi.server.plugin.InstanceNotification;
import com.bea.wlpi.server.plugin.Plugin;
import com.bea.wlpi.server.plugin.PluginManagerCfg;
import com.bea.wlpi.server.plugin.PluginManagerCfgHome;
import com.bea.wlpi.server.plugin.TaskNotification;
import com.bea.wlpi.server.plugin.TemplateDefinitionNotification;
import com.bea.wlpi.server.plugin.TemplateNotification;
import java.lang.reflect.Constructor;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Locale;
import java.net.URL;
import javax.ejb.CreateException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.io.*
/**
* @homeInterface com.bea.wlpi.server.plugin.PluginHome
* @remoteInterface com.bea.testplugin.SamplePlugin
* @statemode Stateless
*/
public class SamplePluginBean implements SessionBean {
private SessionContext ctx;
private final static String PLUGIN_MANAGER_CFG_HOME =
"java:comp/env/ejb/PluginManagerCfg";
private static byte[] ICON_BYTE_ARRAY;

// implements javax.ejb.SessionBean

public void ejbActivate() {
}
    // implements javax.ejb.SessionBean

public void ejbRemove() {
}

// implements javax.ejb.SessionBean

public void ejbPassivate() {
}

// implements javax.ejb.SessionBean

public void setSessionContext(SessionContext ctx) {
this.ctx = ctx;
}
.
.
.

For more information about the plug-in sample, see BPM Plug-In Sample.

 


Plug-In Home Interface

The home interface has been defined for you by the BPM plug-in framework. The com.bea.wlpi.server.plugin.PluginHome interface extends the javax.ejb.EJBHome interface and defines the home interface for all plug-ins.

The following table describes the method defined by the PluginHome home interface.

Table 3-3 Home Interface Method  

Method

Description

public com.bea.wlpi.server.plugin.Plugin create() throws java.rmi.RemoteException java.rmi.RemoteException

Create a remote plug-in object interface.

You can also set up a custom plug-in icon for the WebLogic Integration Studio interface view using the imageStreamToByteArray() method to the com.bea.wlpi.common.plugin.InfoObject. For example:

ICON_BYTE_ARRAY = InfoObject.imageStreamToByteArray(
getClass().getResourceAsStream(
"image")
);

Here image specifies the custom plug-in icon. For more information about the imageStreamToByteArray() method, see InfoObject Object.

When creating value objects for certain plug-in components, you can specify the icon to be used by the Studio to represent the plug-in component when the interface view is enabled. For more information, see Defining Plug-In Value Objects.

This method returns a com.bea.wlpi.server.plugin.Plugin object.

For information about how to enable the interface view within the Studio, see Using the Studio Interface in Using the WebLogic Integration Studio.


 

For more information about the home interface, see the com.bea.wlpi.server.plugin.PluginHome Javadoc.

The following code listing is an excerpt from the plug-in sample that shows how to implement ejbCreate() method for the single create() method that is declared in the home interface, and define a custom plug-in icon for the Studio interface view. This excerpt is taken from the SamplePluginBean.java file in the SAMPLES_HOME/integration/samples/bpm_api/plugin/src/com/bea/wlpi/tour/po/plugin directory.

Listing 3-2 Implementing the Home Interface Method

    // implements javax.ejb.SessionBean

public void ejbCreate() throws CreateException {
try {
ICON_BYTE_ARRAY = InfoObject.imageStreamToByteArray(
getClass().getResourceAsStream("Sample.gif"));
} catch (IOException ioe) {
ioe.printStackTrace();
}
}

For more information about the plug-in sample, see BPM Plug-In Sample.

 


Plug-In Remote Interface

The remote interface is defined for you by the BPM plug-in framework. The com.bea.wlpi.server.plugin.Plugin interface extends the javax.ejb.EJBObject interface and defines the remote interface for all plug-ins.

When defining the plug-in session EJB, you must implement the Plugin remote interface, including its methods.

The Plugin remote interface defines methods in the following categories:

The following sections describe, by category, the Plugin remote interface methods that you must implement.

Note: The contents of the remote interface methods may remain empty, or they may simply return a message to the log; but they must be implemented.

Lifecycle Management Methods

The following table defines the lifecycle management methods that are defined by the remote interface that you must implement. For more information about the BPM plug-in lifecycle, see Managing Lifecycle Tasks.

Table 3-4 Remote Interface Lifecycle Management Methods  

Method

Description

public void load(
com.bea.wlpi.common.plugin.PluginObject
config) throws java.rmi.RemoteException, com.bea.wlip.common.plugin.PluginException

Loads the plug-in using the specified configuration, and registers the plug-in as a notification listener, if specified. For information about registering the plug-in as a notification listener, see Using Plug-In Notifications.

The method parameter is defined as follows:

config:
com.bea.wlpi.common.plugin.ConfigurationInfo object, provided by the com.bea.wlpi.common.plugin.PluginObject object, that specifies the plug-in configuration data.

Note: Plug-ins are responsible for replicating their private cluster-wide state throughout the cluster, if necessary.

public void unload() throws java.rmi.RemoteException, com.bea.wlip.common.plugin.PluginException

Unloads the plug-in, and unregisters it as a notification listener, if required. For information about registering and unregistering the plug-in as a notification listener, see Using Plug-In Notifications.


 

For more information about the methods of managing the remote interface lifecycle, see the com.bea.wlpi.server.plugin.Plugin Javadoc.

Notification Methods

The following table defines the notification methods that are defined by the remote interface that you must implement.

Note: In order to receive notifications, the plug-in must register as a notification listener. For more information, see Using Plug-In Notifications.

Table 3-5 Remote Interface Notification Methods  

Method

Description

public void instanceChanged(
com.bea.wlpi.common.plugin.InstanceNotification
notify) throws java.rmi.RemoteException, com.bea.wlpi.common.plugin.PluginException

Notifies the plug-in of a change to a workflow instance.

The method parameter is defined as follows:

notify:
com.bea.wlpi.server.plugin.InstanceNotification object that specifies the workflow instance change.

public void taskChanged(
com.bea.wlpi.common.plugin.TaskNotification
notify) throws java.rmi.RemoteException, com.bea.wlpi.common.plugin.PluginException

Notifies the plug-in of a change to a task instance.

The method parameter is defined as follows:

notify:
com.bea.wlpi.server.plugin.TaskNotification object that specifies the task change.

public void templateChanged(
com.bea.wlpi.common.plugin.TemplateNotification
notify) throws java.rmi.RemoteException, com.bea.wlpi.common.plugin.PluginException

Notifies the plug-in of a change to a template.

The method parameter is defined as follows:

notify:
com.bea.wlpi.server.plugin.TemplateNotification object that specifies the template change.

public void templateDefinitionChanged(
com.bea.wlpi.common.plugin.TemplateDefinitionNotification
notify) throws java.rmi.RemoteException, com.bea.wliwlpip.common.plugin.PluginException

Notifies the plug-in of a change to a template definition.

The method parameter is defined as follows:

notify:
com.bea.wlpi.server.plugin.TemplateDefinitionNotification object that specifies the template definition change.


 

For more information about remote interface notification methods, see the com.bea.wlpi.server.plugin.Plugin Javadoc.

Plug-In Information Methods

The following table defines the plug-in information methods that are defined by the remote interface that you must implement.

Table 3-6 Remote Interface Plug-In Information Methods  

Method

Description

public java.lang.Class classForName(
java.lang.String
className) throws java.rmi.RemoteException, java.lang.ClassNotFoundException, com.bea.wlpi.common.plugin.PluginException

Instantiates the plug-in-defined class by calling one of the plug-in-supplied metadata objects.

The method parameter is defined as follows:

className:
java.lang.String object that specifies the fully qualified Java class name to instantiate.

This method returns the class with the specified name.

public com.bea.wlpi.common.plugin.PluginDependency[] getDependencies() throws java.rmi.RemoteException

Gets the plug-ins on which the current plug-in depends.

The Plug-in Manager ensures that all dependent plug-ins are loaded before attempting to load the current plug-in.

This method returns a list of com.bea.wlpi.common.plugin.PluginDependency objects. To access information about each plug-in dependency, use the PluginDependency object methods described in PluginCapabilitiesInfo Object.

public java.lang.String getName() throws java.rmi.RemoteException

Gets the globally unique name of the current plug-in, in reverse DNS format.

Note: Reverse DNS format prevents global namespace collisions.

This method returns a java.lang.String object that specifies the unique plug-in name.

public com.bea.wlpi.common.plugin.PluginCapabilitiesInfo getPluginCapabilitiesInfo(
java.util.Locale
lc, com.bea.wlpi.common.plugin.CategoryInfo info) throws java.rmi.RemoteException

Gets detailed information about the plug-in capabilities.

Use this method to:

The method parameters are defined as follows:

  • info:
    com.bea.wlpi.common.plugin.CategoryInfo object that specifies the existing action category tree containing both predefined and plug-in defined actions. A NULL value specifies that the action category information should not be included.

This method returns a com.bea.wlpi.common.plugin.PluginCapabilitiesInfo object, including new categories and actions to be inserted in the action tree, if requested. To access information about the plug-in capabilities, use the PluginCapabilitiesInfo object methods described in PluginCapabilitiesInfo Object.

public com.bea.wlpi.common.plugin.PlugInfo getPluginInfo(java.util.Locale lc) throws java.rmi.RemoteException

Gets basic information about the plug-in.

The method parameter is defined as follows:

lc:
java.util.Locale object that specifies a locale in which to localize display strings.

This method returns a com.bea.wlpi.common.plugin.PluginInfo object. To access information about the plug-in, use the PluginInfo object methods described in PlugInfo Object.

public com.bea.wlpi.common.VersionInfo getVersion() throws java.rmi.RemoteException

Gets the plug-in version information.

This method returns a com.bea.wlpi.common.VersionInfo object. To access information about the plug-in, use the VersionInfo object methods described in "VersionInfo Object" in Value Object Summary in Programming BPM Client Applications.

public void setConfiguration(com.bea.wlpi.common.plugin.PluginObject config) throws java.rmi.RemoteException

Sets the plug-in configuration information.

The method parameter is defined as follows:

config:
com.bea.wlpi.common.plugin.PluginInfo object that specifies the plug-in configuration information.

For more information, see Managing Plug-Ins.


 

For more information about remote interface plug-in information methods, see the com.bea.wlpi.server.plugin.Plugin Javadoc.

Object Manufacturing Method

The following table defines the object manufacturing method that are defined by the remote interface that you must implement.

Table 3-7 Remote Interface Object Manufacturing Method  

Method

Description

public java.lang.Object getObject(
java.util.Locale
lc,
java.lang.String
className) throws java.rmi.RemoteException, java.lang.ClassNotFoundException, com.bea.wlip.common.plugin.PluginException

Gets the plug-in-defined object by calling one of the plug-in-supplied metadata objects.

The method parameters are defined as follows:

This method returns a java.lang.Object instance of the named class.


 

For more information about remote interface object manufacturing method, see the com.bea.wlpi.server.plugin.Plugin Javadoc.

Example of Implementing the Remote Interface

The following code listing is an excerpt from the plug-in sample that shows how to implement the remote interface and its methods. This excerpt is taken from the SamplePluginBean.java file in the SAMPLES_HOME/integration/samples/bpm_api/plugin/src/com/bea/wlpi/tour/po/plugin directory. Notable lines of code are shown in bold.

Listing 3-3 Implementing the Remote Interface

    // Plugin implementation

/**
* Load the Plugin. The plugin should register its
* interest in various system events at this point.
* @param pluginData Plugin configuration data.
* @see PluginManager#addTemplateListener
* @see PluginManager#addTemplateDefinitionListener
* @see PluginManager#addInstanceListener
* @see PluginManager#addTaskListener
* @throws PluginException
*/
public void load(PluginObject pluginData) throws PluginException {

log("load called");
// Enable this block to subscribe to notifications.

/*
PluginManagerCfg pm = null;
try {
pm = getPluginManagerCfg();
Plugin plugin = (Plugin)ctx.getEJBObject();
pm.addInstanceListener(plugin, PluginConstants.EVENT_NOTIFICATION_ALL);
pm.addTaskListener(plugin, PluginConstants.EVENT_NOTIFICATION_ALL);
pm.addTemplateDefinitionListener(plugin, PluginConstants.EVENT_NOTIFICATION_ALL);
pm.addTemplateListener(plugin, PluginConstants.EVENT_NOTIFICATION_ALL);
} catch (Exception e) {
e.printStackTrace();
throw new PluginException(SamplePluginConstants.PLUGIN_NAME, "Unable to get PluginManager");
} finally {
try {
if (pm != null)
pm.remove();
} catch (Exception e) {
}
}
*/
log("loaded");
}

/**
* Unload the plugin. The plugin framework will deregister the plugin
* if it had subscribed to any event notifications.
*/
public void unload() {
log("unload called");
}

public PluginDependency[] getDependencies() {

log("getDependencies called");

return null;
}

public String getName() {
return SamplePluginConstants.PLUGIN_NAME;
}

public VersionInfo getVersion() {
return SamplePluginConstants.PLUGIN_VERSION;
}

/**
* Return descriptive information about the plugin
* @param lc The locale in which to localize display strings.
* @return Descriptive information about the Plugin.
*/
public PluginInfo getPluginInfo(Locale lc) {

log("getPluginInfo called");

return createPluginInfo(lc);
}

public void setConfiguration(PluginObject config) throws PluginException {
}

/**
* Return a complete description of the plugins capabilities. To add new
* subcategories and actions, the plugin should use the category IDs
* passed in via the <code>info</code> parameter passed to identify the
* parent categories, and {@link ActionCategoryInfo#ID_PLUGIN} as the new category ID.
* The PluginManager merges the CategoryInfo array returned by this call with
* the current structure (as passed via the <code>info</code> parameter), and
* replaces references to {@link ActionCategoryInfo#ID_PLUGIN} with newly-assigned
* unique category IDs. The predefined categories have the following IDs:
* {@link ActionCategoryInfo#ID_TASK}, {@link ActionCategoryInfo#ID_WORKFLOW},
* {@link ActionCategoryInfo#ID_INTEGRATION}, {@link ActionCategoryInfo#ID_MISCELLANEOUS},
* {@link ActionCategoryInfo#ID_EXCEPTION}.
* @param lc Locale in which to localize display strings.
* @param info The existing action category tree, containing
* categories and actions both predefined and plugin-defined
* (i.e., by previously loaded plugins).
* @return New categories and actions to be inserted in the tree.
*/
public PluginCapabilitiesInfo getPluginCapabilitiesInfo(Locale lc,
CategoryInfo[] info)
{

PluginInfo pi;
FieldInfo orderFieldInfo;
FieldInfo confirmFieldInfo;
FieldInfo[] fieldInfo;
FunctionInfo fi;
FunctionInfo[] functionInfo;
StartInfo si;
StartInfo[] startInfo;
EventInfo ei;
EventInfo[] eventInfo;
SampleBundle bundle = new SampleBundle(lc);

log("getPluginCapabilities called");

pi = createPluginInfo(lc);
orderFieldInfo =
new FieldInfo(SamplePluginConstants.PLUGIN_NAME, 3,
bundle.getString("orderFieldName"),
bundle.getString("orderFieldDesc"),
SamplePluginConstants.ORDER_FIELD_CLASSES, false);
confirmFieldInfo
=
new FieldInfo(SamplePluginConstants.PLUGIN_NAME, 4,
bundle.getString("confirmFieldName"),
bundle.getString("confirmFieldDesc"),
SamplePluginConstants.CONFIRM_FIELD_CLASSES, false);
fieldInfo
= new FieldInfo[]{ orderFieldInfo, confirmFieldInfo };
ei = new EventInfo(SamplePluginConstants.PLUGIN_NAME, 6,
bundle.getString("confirmOrderName"),
bundle.getString("confirmOrderDesc"), ICON_BYTE_ARRAY,
SamplePluginConstants.EVENT_CLASSES,
confirmFieldInfo);
eventInfo
= new EventInfo[]{ ei };
fi = new FunctionInfo(SamplePluginConstants.PLUGIN_NAME, 7,
bundle.getString("calcTotalName"),
bundle.getString("calcTotalDesc"),
bundle.getString("calcTotalHint"),
SamplePluginConstants.FUNCTION_CLASSES, 3, 3);
functionInfo
= new FunctionInfo[]{ fi };
si = new StartInfo(SamplePluginConstants.PLUGIN_NAME, 5,
bundle.getString("startOrderName"),
bundle.getString("startOrderDesc"), ICON_BYTE_ARRAY,
SamplePluginConstants.START_CLASSES, orderFieldInfo);
startInfo
= new StartInfo[]{ si };

PluginCapabilitiesInfo pci = new PluginCapabilitiesInfo(pi,
getCategoryInfo(bundle), eventInfo,
fieldInfo, functionInfo, startInfo,
null, null, null, null, null);

return pci;
}

/**
* Return a plugin-defined class. The caller retrieves the class name by
* calling on one of the plugin-supplied metadata objects.
* @param className The fully qualified Java class name to instantiate.
* @return The class with the specified name.
* @throws ClassNotFoundException if the plugin could not load the class.
* @see ActionInfo
* @see EventInfo
* @see FunctionInfo
* @see FieldInfo
* @see PluginInfo
* @see StartInfo
* @see DoneInfo
* @see VariableTypeInfo
* @see TemplatePropertiesInfo
* @see TemplateDefinitionPropertiesInfo
* @throws PluginException
*/
public Class classForName(String className)
throws ClassNotFoundException, PluginException {

log("classForName called");

return Class.forName(className);
}

/**
* Return a plugin-defined object. The caller retrieves the class name by
* calling on one of the plugin-supplied metadata objects.
* @param lc The locale in which to localize display strings.
* @param className The fully qualified Java class name to instantiate.
* @return
* @see ActionInfo
* @see EventInfo
* @see FunctionInfo
* @see FieldInfo
* @see PluginInfo
* @see StartInfo
* @see DoneInfo
* @see VariableTypeInfo
* @see TemplateInfo
* @see TemplateDefinitionInfo
* @throws ClassNotFoundException
* @throws PluginException
*/
public Object getObject(Locale lc, String className)
throws ClassNotFoundException, PluginException {

log("getObject called with class name " + className);

try {
Class cls = Class.forName(className);
Object obj;

try {
// see if there is a ctor that takes a Locale object
Class[] paramType = new Class[]{ lc.getClass() };
Constructor ctor = cls.getConstructor(paramType);
Object[] param = new Object[]{ lc };

obj = ctor.newInstance(param);

return (obj);
} catch (Exception e) {
}

// if not ctor takes a Locale just call the no-arg ctor
return Class.forName(className).newInstance();
} catch (InstantiationException ie) {
throw new PluginException(SamplePluginConstants.PLUGIN_NAME,
"Unable to instantiate class: " + ie);
} catch (IllegalAccessException iae) {
throw new PluginException(SamplePluginConstants.PLUGIN_NAME,
"Unable to instantiate class: " + iae);
}
}

/**
* Notifies a plugin of a change in a template.
* @param e An event object describing the change.
*/
public void templateChanged(TemplateNotification e) {
log("templateChanged called");
}

/**
* Notifies a plugin of a change in a template definition.
* @param e An event object describing the change.
*/
public void templateDefinitionChanged(TemplateDefinitionNotification e) {
log("templateDefinitionChanged called");
}

/**
* Notifies a plugin of a change in a workflow instance.
* @param e An event object describing the change.
*/
public void instanceChanged(InstanceNotification e) {
log("instanceChanged called");
}

/**
* Notifies a plugin of a change in a task instance.
* @param e An event object describing the change.
*/
public void taskChanged(TaskNotification e) {
log("taskChanged called");
}
    private PluginInfo createPluginInfo(Locale lc) {

HelpSetInfo helpSet;
PluginInfo pi;
SampleBundle bundle = new SampleBundle(lc);
String name = bundle.getString("pluginName");
String desc = bundle.getString("pluginDesc");
String helpName = bundle.getString("helpName");
String helpDesc = bundle.getString("helpDesc");

helpSet = new HelpSetInfo(SamplePluginConstants.PLUGIN_NAME, helpName,
helpDesc,
new String[]{ "htmlhelp/Sample", "index" },
HelpSetInfo.HELP_HTML);
pi = new PluginInfo(SamplePluginConstants.PLUGIN_NAME, name, desc, lc,
SamplePluginConstants.VENDOR_NAME,
SamplePluginConstants.VENDOR_URL,
SamplePluginConstants.PLUGIN_VERSION,
SamplePluginConstants.PLUGIN_FRAMEWORK_VERSION,
null, null, helpSet);

return pi;
}

// It is necessary to create these objects afresh each time, because we
// relinquish ownership of the result and the plugin framework assigns
// a system ID to each item. Reassignment will cause an
// IllegalStateException.
private CategoryInfo[] getCategoryInfo(SampleBundle bundle) {

ActionInfo checkInventoryAction =
new ActionInfo(SamplePluginConstants.PLUGIN_NAME, 1,
bundle.getString("checkInventoryName"),
bundle.getString("checkInventoryDesc"), ICON_BYTE_ARRAY,
ActionCategoryInfo.ID_NEW,
ActionInfo.ACTION_STATE_ALL,
SamplePluginConstants.CHECKINV_CLASSES);
ActionInfo sendConfirmAction =
new ActionInfo(SamplePluginConstants.PLUGIN_NAME, 2,
bundle.getString("sendConfirmName"),
bundle.getString("sendConfirmDesc"), ICON_BYTE_ARRAY,
ActionCategoryInfo.ID_NEW,
ActionInfo.ACTION_STATE_ALL,
SamplePluginConstants.SENDCONF_CLASSES);
ActionCategoryInfo[] actions =
new ActionCategoryInfo[]{ checkInventoryAction, sendConfirmAction };
CategoryInfo[] catInfo =
new CategoryInfo[]{ new CategoryInfo(SamplePluginConstants.PLUGIN_NAME,
0, bundle.getString("catName"),
bundle.getString("catDesc"),
ActionCategoryInfo.ID_NEW,
actions) };

return catInfo;
}

private void log(String msg) {
System.out.println("SamplePlugin: " + msg);
}

For more information about the plug-in sample, see BPM Plug-In Sample.

 

Back to Top Previous Next