How-To Creating a Connector Plug-In
Prerequisites
Study the material on how-to create a Java plug-in for publishing server per-se.
Study the material on how-to define entity models and on data flow in publishing hub per-se.
Specify the use of connector entity identifiers for your connector. These identifiers are simple strings that can be used to build requests to your content system. They can be simple names that correspond to detailed definition in some configuration file, or the can be strings like query parameters of a URL that are readable and can be parsed in a clean way. Specification issues are outside the scope of this document.
Introduction
Since version 4.0.5 the recommended way of creating a connector is by sub-classing from the AbstractConnector class contained in the PubServerSDK.
You will have to create three classes:
- Connector Library Class derived from AbstractConnector.Library.
- Connector Configuration Class derived from AbstractConnector.Configuration.
- Connector Class itself derived from AbstractConnector
To create a syntactically correct connector will have to go through a short list of steps. We will provide an example called “MyConnector” done by a company called “MyCompany”.
Creating a Connector Step by Step
Create Eclipse Project
The following steps are not different from any other plug-in:
- Create an EJB project in Eclipse called “MyConnector”.
- Bind the project to the Payara Runtime.
- Add the pubserver user libraries into the build path.
Create Package
In the source folder of the project we create a package named
package tdl.mycompany.pubserver.connector.myconnector;
Create Connector Library
In the package we create a class called MyConnectorLibrary, containing the following code:
@Singleton
@Startup
@PubServerPluginLibrary(
id=" tdl.mycompany.pubserver.connector.myconnector",
vendor = “mycompany.tdl",
version="1.0",
type = PluginType.CONNECTOR,
configClasses = {MyConnectorConfiguration.class}
)
public class MyConnectorLibrary extends AbstractConnector.Library {
// intentionally left empty
}
You won’t need more in most situations. The only thing worth to know is, that this library class has to be a singleton. This makes it the first choice if you want to use some cache within your connector that may be used from different threads. Simple caching can be obtained by using the instanceData object (see below).
Create Connector Configuration
We create a second class “MyConnectorConfiguration” containing JAXB annotation. This class represent the XML configuration that will be saved in the repository of publishing server via ison.
Only as an example we add one field containing a “connection-string”. In the real world we may e.g. add fields for servername, context-path, user and password for a RESTService. We may also add fields to control the interpretation of connector entity identifiers.
@XmlRootElement(name = "my-config")
@XmlAccessorType(XmlAccessType.FIELD)
public class MyConnectorConfiguration extends AbstractConnector.Configuration {
@XmlElement(name = "connection-string")
private String connectionString;
public final String getConnectionString() {
return this.connectionString;
}
public final void setConnectionString(String connectionString) {
this.connectionString = connectionString;
}
}
Create Connector Main Class
We create a third class names “MyConnector”. It could be as simple as this:
@Stateless(mappedName = MyConnector.MAPPED_NAME)
@PubServerPlugin
public class MyConnector extends AbstractConnector implements ConnectorLocal {
protected static final Logger LOGGER = LoggerFactory.getLogger(MyConnector.class);
public static final String MAPPED_NAME = “tdl.mycompany.pubserver.connector.myconnector.MyConnector";
@EJB
private MyConnectorLibrary pluginLibrary;
@Override
protected Class<? extends Configuration> getConfigurationClass() {
return MyConnectorConfiguration;
}
@Override
protected Library getConnectorLibrary() {
return this.pluginLibrary;
}
@Override
protected Logger getLogger() {
return LOGGER;
}
@Override
protected String getMappedName() {
return MAPPED_NAME;
}
}
This makes up a correct and complete connector even though we lack meaningful getter methods reading from the content system.
Create Getter and Setter Methods
Starting with getRootBuckets we can now add getter and setter methods for the connector – thus overwriting the empty defaults from AbstractConnector that will all return a NotImplemented exception.
Methods must wrap their call in a try catch to make sure that only checked exceptions of type DataSourceException, or ConnectorException are thrown.
Real implementation of how to access the content system and how to interpret connector entity identifiers are outside the scope of this document.
@Override
@PubServerMethod(type = PluginMethod.MethodType.CONNECTOR, description = "")
public List<Bucket> getRootBuckets(Context context, ConnectorEntity connectorEntity, String searchStr) throws NotImplementedException, DataSourceException, ConnectorException {
try {
// your code goes here...
// on most cases you will build a request object from the context,
// the connector entity identifier, and optionally the search string
// execute it on the target system using credentials defined in the
// configuration
// map the result to entitydata objects like buckets and prices
} catch (DataSourceException | ConnectorException e) {
throw e;
} catch (Throwable e) {
throw new MyConnectorException("Unknown exception", e);
}
}
Next step is to implement all other methods needed. Most prominently: getChildBuckets, getBucketsByIdentifier, getTextsOfBucket, getTextsByIdentifer. But which methods you need and which ones you can support will depend on your content system and your connector design.
Using the InstanceData Object
The AbstractConnector has a build-in capability of caching one data object for an instance. Filling this object can be seen as a kind of initialization of the connector instance.
You are free to define your own type. In most cases this object will be used to store instance specific data that are common to most requests to the content system. E.g. the list of supported languages.
To use the InstanceData capability you have first to create at least one custom data class, and then to override the fetchInstanceData method of the connector class to return instances of your data class. Later you can retrieve the data in methods like getRootBuckets by get calling the getInstanceData method of the connector.
Define an Instance Data Type
Instance Data can be of any type. The related methods just store it as an “Object” in the cache. You are responsible for casting the value to your class.
Here is an example class
class MyInstanceData {
public final List<String> someStrings new ArrayList<>();
}
Implement fetchInstanceData Method to Set Instance Data
This method is automatically called if you call getInstanceData(instance) on the connector and no data are already existing. In most cases there is no need to call it in your own code. Resulting data for different instances will be stored in a ConcurrentHashMap of the AbstractConnector.Library. Data will stay there until you explicitly drop them by a call to invalidateInstanceData(instance).
@Override
public Object fetchInstanceData(Configuration connectorConfig) {
MyInstanceData data = new MyInstanceData();
// connect to the content system
// read initial data
// add initial data to the custom object
data.someStrings.add(“my language data what ever it is”);
// return the object
return data;
}
Use getInstanceData Method to Retrieve Instance Data
@Override
public List<Bucket> getRootBuckets(Context context, ConnectorEntity connectorEntity, String searchStr) throws NotImplementedException, DataSourceException, ConnectorException {
try {
// retrieve instance data
String instance = connectorEntity.getInstance();
MyInstanceData data = (MyInstanceData) this.getInstanceData(instance);
// use data in following code...
} catch (DataSourceException | ConnectorException e) {
throw e;
} catch (Throwable e) {
throw new MyConnectorException("Unknown exception", e);
}
}
If you call getInstanceData on the connector it will look if data have already been retrieved for the instance. If so this object is immediately returned. Only if there are no data for the instance the connector will call fetchInstanceData. To drop existing data, please call invalidateInstanceData(instance).
Invalidating the Instance Data
Instance data will stay in the internal cache until you explicitly drop them. To drop them call invalidateInstanceData(instance) on the connector.
this.invalidateInstanceData("MyInstance");
Adding a Default Configuration
If your system can be run without explicit configuration you can make use of the default configuration feature. E.g. if the connector uses a database connection from application servers connection pool, you will most probably have a default connection resource name and may want to override it only if there is a need to configure more than one instance of the connector. But even in cases with one instance only you may want to override the build-in default from the Java code. These use cases are covered by the “Default Configuration” feature of the abstract connector.
To activate the feature you have to override the implementation of allowDefaultInstance to return “true”. And – of course – the configuration class should initialize its fields with appropriate values.
For more details look into the Javadoc of the AbstractConnector.Configuration.
In the example using the MyConnectorConfiguration class we would set an initial value for the connectionString field. And override the implementation of allowDefaultInstance to return “true”.
@XmlElement(name = "connection-string")
private String connectionString = "jdbc/MyConnector";
@Override
public boolean allowDefaultInstance () {
return true;
}