This manual will show you how to implement an extended version of the generic filesystem connector available to all TranslationStudio modules.
The connector will allow you to add any number of projects to the connector with their own settings:
Each connector consists of 2 projects:
This example connector implementation will assume Netbeans as your IDE.
You will find a Javadoc available at api/javadoc/connector
First, create a FirstSpirit module maven project CONNECTOR - Example - Frontend using the project template provided in your TranslationStudio package at
api/project-templates/module
Second, create a Connector maven project CONNECTOR - Example - Backend using the project template provided in your TranslationStudio package at
api/project-templates/connector
The POM files will require you to add the following dependencies to your projects:
/api/
.
Open the newly created project CONNECTOR - Example - Frontend and create a new JPanel class
com.idmedia.translationstudio.connector.example.configuration.ConfigurationPanel
extending javax.swing.JPanel
Now, design the panel similar to this
This tutorial will not provide code example to design the GUI but will only focus on the methods relevant to the connector.
The JList on the left should have the following declaration
javax.swing.JList<IConfiguredProject> jListProjects
Create the following member variable
private final DefaultListModel<ConfiguredProject> m_pProjectListModel = new DefaultListModel();
and assign it as model to
jListProjects
In order to make the panel available to TranslationStudio, add the following method:
/**
* Get the configuration panel instance to be displayed by TranslationStudio
* @return Panel
*/
@NotNull
JPanel getPanel()
{
return this;
}
To store potential changes made to the projects from the list, add the following methods:
/** * Create a configuration that will be passed on to the init method of this class * to initialize the configuration panel. * @return */ @NotNull String onSaveGuiConfiguration() { return onSaveConnectorConfiguration(); } /** * Create a configuration that will be used by the connector implementation. * @return */ @NotNull String onSaveConnectorConfiguration() { final StringBuilder psXml = new StringBuilder(0); psXml.append("<configuration><projects>"); final DefaultListModel pModel = m_pProjectListModel; final int nSize = pModel.getSize(); for (int i = 0; i < nSize; i++) psXml.append(((ConfiguredProject)pModel.get(i)).toGuiXml()); psXml.append("</projects>"); psXml.append("</configuration>"); return psXml.toString(); }
These methods will basically create an XML with all projects available to the list component. The very same XML will also be used to initialise the panel on startup.
Since this panel will now be accessible, we need to add a method to initialise it once it has been created.
Therefore, add the following method and ignore potential errors for the moment:
/**
* Init the panel right after it is being displayed
* @param sXml Gui Configuration as provided by onSaveGuiConfiguration()
* @param vpJobs List of available job export configurations
*/
void onInit(String sXml, List<IProjectJobConfigurtion> vpJobs)
{
if (vpJobs != null)
m_vpJobs.addAll(vpJobs);
/**
* add all jobs
*/
for (IProjectJobConfigurtion _job : m_vpJobs)
jJobs.addItem(_job);
/**
* disable fields
*/
enableEditableFields(false);
/**
* load from xml
*/
final List<ConfiguredProject> vpElements = ConfiguredProject.fromXml(sXml);
/**
* add all projects
*/
final DefaultListModel pModel = (DefaultListModel) jListProjects.getModel();
for (ConfiguredProject _prj : vpElements)
pModel.addElement(_prj);
/**
* select first project
*/
selectProject(0);
/**
* enable fields
*/
if (!m_pProjectListModel.isEmpty())
enableEditableFields(true);
}
Finally, we have to provide a list of configured projects ready to be used by TranslationStudio. Therefore, add the following method:
/** * Create a list of configured projects that will be used by TranslationStudio and eventually made available * to the language mappings dropdown component. * @return */ @NotNull List<ConfiguredProject> getConfiguredProjects() { final List<ConfiguredProject> vpList = new ArrayList<>(); final DefaultListModel pModel = m_pProjectListModel; final int nSize = pModel.getSize(); for (int i = 0; i < nSize; i++) vpList.add(((ConfiguredProject)pModel.get(i)).duplicate()); return vpList; }
The methods above will require a new class ConfiguredProject
to be created.
A sample implementation may look like the following - extra work required!
class ConfiguredProject extends XmlStorable<ConfiguredProject> { private static final Logger LOGGER = Logger.getLogger(ConfiguredProject.class.getName()); private String displayName; private String id; private String jobConfigurationId; private String notificationAddress; private String copyToDirectory; private boolean keepOriginalFiles = true; private String commandline; protected ConfiguredProject() { this(true); } protected ConfiguredProject(boolean bCreateId) { if (bCreateId) id = UUID.randomUUID().toString(); displayName = ""; jobConfigurationId = ""; copyToDirectory = ""; commandline = ""; } /** * Clone this project * @return */ @NotNull @Override public ConfiguredProject duplicate() { final ConfiguredProject pThat = new ConfiguredProject(false); pThat.displayName = this.displayName; pThat.id = this.id; pThat.jobConfigurationId = this.jobConfigurationId; pThat.keepOriginalFiles = this.keepOriginalFiles; pThat.notificationAddress = this.notificationAddress; pThat.copyToDirectory = this.copyToDirectory; pThat.commandline = this.commandline; return pThat; } /** * Create an XML representation of this project * @return */ @NotNull @Override public String toGuiXml() { final StringBuilder psXml = new StringBuilder(); psXml.append("<roject>"); psXml.append(createTag("id", id)); psXml.append(createTag("displayName", displayName)); psXml.append(createTag("jobConfigurationId", jobConfigurationId)); psXml.append(createTag("email", notificationAddress)); psXml.append(createTag("copyto", "" + copyToDirectory)); psXml.append(createTag("keepfiles", keepOriginalFiles ? "true" : "false")); psXml.append(createTag("commandline", commandline)); psXml.append("</roject>"); return psXml.toString(); } /** * Create an XML representation of this project * @return */ @NotNull @Override public String toConnectorXml() { return toGuiXml(); } /** * Create a list of projects from a stored xml * @param sXml * @return */ @NotNull public static List<ConfiguredProject> fromXml(final String sXml) { final List<ConfiguredProject> vpList = new ArrayList<>(); if (sXml == null || sXml.isEmpty()) return vpList; int nStart = sXml.indexOf("<project>"); while (nStart >= 0) { int nEnd = sXml.indexOf("</project>", nStart); if (nEnd < nStart) break; final ConfiguredProject pProject = fromSingleXml(sXml.substring(nStart, nEnd + 1)); if (pProject != null) vpList.add(pProject); else LOGGER.log(Level.WARNING, "Could not create project from xml {0}", sXml.substring(nStart, nEnd + 1)); nStart = sXml.indexOf("<project>", nEnd); } return vpList; } /** * Create a project from its stored xml * * @param sXml * @return */ @Nullable private static ConfiguredProject fromSingleXml(final String sXml) { if (sXml == null || sXml.isEmpty()) return null; final ConfiguredProject pPrj = new ConfiguredProject(false); pPrj.id = pPrj.extractSimpleTag("id", sXml); pPrj.displayName = pPrj.extractSimpleTag("displayName", sXml); pPrj.jobConfigurationId = pPrj.extractSimpleTag("jobConfigurationId", sXml); pPrj.notificationAddress = pPrj.extractSimpleTag("email", sXml); pPrj.keepOriginalFiles = "true".equals(pPrj.extractSimpleTag("keepfiles", sXml)); pPrj.copyToDirectory = pPrj.extractSimpleTag("copyto", sXml); pPrj.commandline = pPrj.extractSimpleTag("commandline", sXml); return pPrj.id == null || pPrj.id.isEmpty() ? null : pPrj; } /** * Update Display name * @param displayName */ public void setDisplayName(String displayName) { this.displayName = displayName; } /** * Update internal id * @param id */ public void setId(String id) { this.id = id; } public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress == null ? "" : notificationAddress; } public String getNotificationAddress() { return notificationAddress == null ? "" : notificationAddress; } /** * Update Job Configuraiton Id * @param jobConfigurationId */ public void setJobConfigurationId(String jobConfigurationId) { this.jobConfigurationId = jobConfigurationId; } @NotNull public String getDisplayName() { return displayName == null ? "" : displayName; } /** * Get the project id TranslationStudio uses * @return */ @NotNull public String getId() { return id == null ? "" : id; } /** * Get Job Configuration Id * @return */ @NotNull public String getJobConfigurationId() { return jobConfigurationId == null ? "" : jobConfigurationId; } @Override public String toString() { return getDisplayName(); } public boolean keepOriginalFiles() { return keepOriginalFiles; } public void keepOriginalFiles(boolean bKeep) { keepOriginalFiles = bKeep; } /** * Create a hash * @return */ @Override public int simpleHash() { int hash = 1; hash = hash * 17 + getId().hashCode(); hash = hash * 22 + getJobConfigurationId().hashCode(); hash = hash * 25 + getNotificationAddress().hashCode(); hash = hash * 25 + getDisplayName().hashCode(); hash = hash * 26 + (""+keepOriginalFiles()).hashCode(); hash = hash * 27 + getCommandLine().hashCode(); hash = hash * 27 + getCopyToDirectory().hashCode(); return hash; } String getCommandLine() { return commandline; } void setCommandLine(String text) { commandline = text; } String getCopyToDirectory() { return copyToDirectory; } void setCopyToDirectory(String sDir) { copyToDirectory = sDir; } }
Finally, it is time to allow TranslationStudio to access the connector with all its nice features. Therefore, create a new class
ConnectorConfigurationPanel
implementing
ITranslationMemorySystemConnectorConfigurationGui
The interface requires us to implement quite a number of methods, so we will dive in right away.
First, we need some member variables:
/** * Store connector configuration */ private String m_sXmlConnector = ""; private String m_sXmlGui = ""; /** * Talk to the TranslationStudio Application */ private IExchangeCommunicator m_pCommunicator = null; /** * Hold instance of panel */ private ConfigurationPanel m_pPanel = null; /** * List of projects available to the language mapping */ private final List<IProjectJobConfigurtion> m_vpJobs = new ArrayList<>();
Second, some methods provide functionality not needed here, so we get rid of these right now:
/** * {@inheritDoc } */ @Override public void onCreated() { } /** * {@inheritDoc } */ @Override public void onShow() { }
Third, let’s initialise the connector configuration properly by saving the data provided once the connector has been created by the TranslationStudio Configuration Panel itself. This will allow us to the panel as it should be.
/** * {@inheritDoc } */ @Override public void onInit(String sId, String sConfiguration, List<IProjectJobConfigurtion> vsAvailableConfigurations, IExchangeCommunicator pCommunicator) { m_sXmlGui = sConfiguration == null ? "" : sConfiguration; if (vsAvailableConfigurations != null) m_vpJobs.addAll(vsAvailableConfigurations); m_pCommunicator = pCommunicator; }
Fourth, what is a connector without a proper name:
/** * {@inheritDoc } */ @Override public String getName() { return "Example Connector"; }
Fifth, we have to provide the panel instance itself and initialise it properly. Otherwise, nothing will be seen at all…
/**
* {@inheritDoc }
*/
@Override
public JPanel getConfigurationPanel()
{
if (m_pPanel == null)
{
m_pPanel = new ConfigurationPanel(m_pCommunicator);
m_pPanel.onInit(m_sXmlGui, m_vpJobs);
}
return m_pPanel;
}
Sixth, saving everything and making it available is quite easy since we can delegate that to the panel itself:
/** * {@inheritDoc } */ @Override public void onSave() { m_sXmlConnector = m_pPanel == null ? "" : m_pPanel.onSaveConnectorConfiguration(); m_sXmlGui = m_pPanel == null ? "" : m_pPanel.onSaveGuiConfiguration(); } /** * {@inheritDoc } */ @NotNull @Override public String getGuiConfiguration() { return m_sXmlGui; } /** * {@inheritDoc } */ @NotNull @Override public String getConnectorConfiguration() { return m_sXmlConnector; }
Finally, we have to make configured projects available to TranslationStudio. These can be selected in the language mapping page template.
/** * {@inheritDoc } * @return */ @Override public List<IConfiguredTranslationMemoryProject> getProjects() { final List<IConfiguredTranslationMemoryProject> vpList = new ArrayList<>(); final List<ConfiguredProject> vpProjects = m_pPanel.getConfiguredProjects(); for (ConfiguredProject _prj : vpProjects) { final ConfiguredProject pProject = _prj.duplicate(); final IConfiguredTranslationMemoryProject _project = new IConfiguredTranslationMemoryProject() { @Override public String getId() { return pProject.getId(); } @Override public String getName() { return pProject.getDisplayName(); } @Override public List<String> getNotificationEmails() { return new ArrayList<>(); } @Override public String getJobId() { return pProject.getJobConfigurationId(); } }; vpList.add(_project); } return vpList; }
You can rename your FSM module by editing the file ./src/main/resources/module.xml
In addition, you can rename the FSM file itself by ./pom.xml
file
That was easy, was it not? Now that we have mastered the configuration panel, we can implement the connector itself.
Open the newly created project CONNECTOR - Example - Backend.
This project will provide the business logic of the connector.
com.idmedia.translationstudio.connector.example.impl.Connector
implementing ITranslationMemoryConnector
The connector has three tasks to perform:
/** * Operation to transfer files into the TMS * @return */ public ITranslationOperation getTranslationOperation(); /** * Operation to query registered files * @return */ public ITranslationStatusQueryOperation getTranslationStatusQueryOperation(); /** * Operation to manage queries from the connector's configuration panel (ServerManager) * @param sQueryData Input data * @return */ public IQueryOperation getQueryOperation(String sQueryData);
Each of the first two operations will be requested from TranslationStudio after the setup method has been called:
/** * Setup the connector * * @param sTmsId Tms Id * @param sConfigurationXml Connector Configuration (as provided by {@link com.idmedia.translationstudio.api.connector.gui.ITranslationMemorySystemConnectorConfigurationGui.getConnectorConfiguration} * @param sWorkingDirectory Connector Working Directory * @param pContext Connector Context * @return */ public boolean setup(String sTmsId, String sConfigurationXml, String sWorkingDirectory, IConnectorContext pContext);
There are 2 essential parameters:
sConfigurationXml
ITranslationMemorySystemConnectorConfigurationGui.getConnectorConfiguration()
pContext
In addition, you may react to any changes to your connector configuration (i.e. whenever the configuration has been updated using TranslationStudio’s configuration panel):
/** * Method called when the settings are updated using the ServerManager * @param sConfigurationXml Connector Configuration (as provided by {@link com.idmedia.translationstudio.api.connector.gui.ITranslationMemorySystemConnectorConfigurationGui.getConnectorConfiguration} */ public void onUpdateConfiguration(String sConfigurationXml);
Your implementation may access the entire TranslationStudio API via |
Your implementation of the setup method has to evaluate the xml provided by the connector configuration panel. Since this is simple XML evaluation, I leave this up to you. However, you have to store the result in a member variable, for example:
/**
* Hold the configuration
*/
private final Configuration m_pConfiguration = new Configuration();
/**
* {@inheritDoc }
*/
@Override
public boolean setup(String sTmsId, String sConfigurationXml, String sWorkingDirectory, IConnectorContext pContext)
{
m_pConfiguration.load(sConfigurationXml);
return true;
}
We do not want to be shy so there is no need to hide the name and version of this wonderful connector:
/** * {@inheritDoc } */ @Override public String getName() { return "Extended Filesystem Connector"; } /** * {@inheritDoc } */ @Override public String getVersion() { return "2.3.0"; /* I am lazy, so I simply use the TranslationStudio Version */ }
The three operations will be implemented in their own classes, so it will be easy here:
/** * {@inheritDoc } */ @Override public ITranslationOperation getTranslationOperation() { return new TranslationOperation(m_pConfiguration); } /** * {@inheritDoc } */ @Override public ITranslationStatusQueryOperation getTranslationStatusQueryOperation() { return new TranslationStatusQueryOperation(); } /** * {@inheritDoc } */ @Override public IQueryOperation getQueryOperation(String sQueryData) { return new QueryOperation(); }
This connector does not need to react to configuration changes. However, if your connector wants provide a web interface which has to connect to a third party system, you may want to use this method to store the configuration in your own configuration file. This file may, in turn, be loaded by the web interface when required. For now, we do not need it.
/** * {@inheritDoc } */ @Override public void onUpdateConfiguration(String sConfigurationXml) { /* not needed */ }
It is possible to perform additional tasks after translatable XMLs have been processed using the
ITranslationOperation
. This connector, however, does not need this capability.
/** * {@inheritDoc } */ @Override public void onPerformAdditionalTasks() { /* not needed */ }
The method public IQueryOperation getQueryOperation(String sQueryData)
allows your connector’s configuration panel to query data at runtime. For example, if you connect to
a third party system and want to load all available projects and list them in the configuration panel,
this method is what you are looking for.
The configuration panel can use the method IExchangeCommunicatorsendMessage(String sMessage, IQueryRequestAnswerReceived pOnAnswerCallback);
to send a message to an instance of this connector. Importantly, the setup method will not be called.
Hence, all data necessary to setup the connector have to be sent in the message itself (i.e. credentials etc.)
This connector does not need such a feature, so we use an empty implementation:
/** * Do nothing */ private static class QueryOperation implements IQueryOperation { /** * {@inheritDoc } */ @Override public void query() { } /** * {@inheritDoc } */ @NotNull @Override public String getResult() { return ""; } }
Whenever your connector has to handle new translatable XMLs, an instance of the ITranslationOperation
is requested.
This connector has the following features:
First, create a new class TranslationOperation
implementing ITranslationOperation
As shown above, the class will get a configuration object and your minimal initial class needs have the following elements:
private static final Logger LOGGER = Logger.getLogger(TranslationOperation.class.getName()); private final List<ITranslatableFile> m_vpTranslatableFiles = new ArrayList<>(0); private final List<String> m_vsCopiedSuccessfully = new ArrayList<>(0); private final List<String> m_vsCopiedErrorneous = new ArrayList<>(0); private final List<String> m_vsRemovable = new ArrayList<>(0); private boolean m_bKeppOriginalFiles = true; private final IFileSystemConfiguration m_pConfiguration; TranslationOperation(@NotNull IFileSystemConfiguration pConfiguration) { m_pConfiguration = pConfiguration; }
Second, store all files that have to be processed:
/**
* {@inheritDoc }
*/
@Override
public void addTranslatableFiles(List<ITranslatableFilegt; vpList)
{
if (vpList == null || vpList.isEmpty())
return;
m_vpTranslatableFiles.addAll(vpList);
}
Once all translatable xml files have been added to the operation it can be executed:
/** * {@inheritDoc } */ @Override public boolean perform() { boolean bSuccess = true; for (ITranslatableFile _file : m_vpTranslatableFiles) { if (!perform(_file)) bSuccess = false; } return bSuccess; } /** * Process a single file * * @param pFile * @return */ private boolean perform(ITranslatableFile pFile) { clearLists(); final String sProjectId = pFile.getProjectId(); if (sProjectId == null || sProjectId.isEmpty()) return false; final IFileSystemProject pConfig = m_pConfiguration.getConfiguredProject(sProjectId); /* Copy files */ final boolean bCopy = copyTranslatableFiles(pConfig.copyToTargetDirectory(), pConfig.keepOriginalTranslatableFiles()); /* remove files */ removeSourceFiles(); /* execute shell command */ final String sCommandResult = new ExecuteShellCommandOperation().perform(pConfig.getCommandLineExecutionString()); /* send email */ sendMail(pConfig.getNotificationMail(), sCommandResult); clearLists(); return bCopy; }
The above will process each file and copy them if necessary, execute a system command and send an email.
However, this tutorial will not consider these features and leave it up to you. Yet, you may access send emails using the TranslationStudio API method TranslationStudio.get().requestCustomEmailService().sendMail(…)
,
for example:
/** * Send email * @param sMailTo * @return */ private boolean sendMail(@Nullable String sMail, @NotNull String sShellCommandResult) { if (sMailTo == null || !sMailTo.contains("@")) return false; final String sBody = MailSummaryComposer.compose(m_vsCopiedSuccessfully, m_vsCopiedErrorneous, m_vsRemovable, sShellCommandResult); return TranslationStudio.get().requestCustomEmailService().sendMail(sMailTo, "TranslationStudio: Translatable File(s) available", sBody, false); }
Third, implement deprecated methods:
/** * {@inheritDoc } */ @Override public List<Long> getSuccessfulJobs() { return new ArrayList<>(0); } /** * {@inheritDoc } */ @NotNull @Override public List<Long> getFailedJobs() { return new ArrayList<>(0); }
Forth, if your connector does or does not require the source files anymore, you may say so.
/**
* {@inheritDoc }
*/
@Override
public boolean requireSourceTanslatableFiles()
{
/**
* the connector manages file removal
*/
return true;
}
Fifth, if you have to monitor the translation process of the processed files, you may
provide information to TranslationStudio. The files will be queried regularly using the
ITranslationStatusQueryOperation
(not needed here).
/** * {@inheritDoc } */ @NotNull @Override public List<ITransferedFile> getResult() { return new ArrayList<>(0); }
Sixth, all files which could not be processed and should be processed again can be provided as well (not needed here):
/** * {@inheritDoc } */ @NotNull @Override public List<ITranslatableFile> getTranslatableFilesToRetry() { return new ArrayList<>(0); }
TranslationStudio will check the translation status of all transferred files regularly. Your connector
has to implement ITranslationStatusQueryOperation
to do this.
This connector does not need to check for the status since it only deploys the XMLs files to the filesystem. Therefore, the following implementation will suffice:
class TranslationStatusQueryOperation implements ITranslationStatusQueryOperation { /** * {@inheritDoc } */ @Nullable @Override public byte[] downloadTranslation(String sProjectId, String sFileId) { return null; } /** * {@inheritDoc } */ @Override public void addFileToMonitor(String sFileId, String sProjectId) { } /** * {@inheritDoc } */ @NotNull @Override public List<IFileStatus> query() { return new ArrayList<>(0); } }
However, if you need to check the status, you first have to store all files to be monitored in a list in the method void addFileToMonitor(String sFileId, String sProjectId)
.
This method assumes that your target system that received the XML files returned a unique id for each file, hence the two parameters.
The method List<IFileStatus> query()
then querys the status and provides a list of status information with one list entry per added file.
If the a file status equals Status.FINISHED
, the file will be downloaded immediately using the method byte[] downloadTranslation(String sProjectId, String sFileId)
.
The file will be stored in the working/translated
directory and imported into FirstSpirit.
If you want to remove a file from the monitoring process (e.g. because it is not available anymore), simple set the status to Status.NOTFOUND
.
TranslationStudio will remove the file from the database automatically.
Deploying a connector consists of 2 steps
./connectors
directory.
To access the connector using TranslationStudio’s configuration panel, please consult the installation manual.
However, to allow for an easy installation, you may want to create your own install xml file. A sample file is provided in the sample connector project under ./src/main/resources/example-connector.xml
<?xml version="1.0" encoding="UTF-8"?> <connector> <name>Example Connector</name> <instance>com.idmedia.translationstudio.connector.example.impl.Connector</instance> <configurable>com.idmedia.translationstudio.connector.example.configuration.ConnectorConfigurationPanel</configurable> <xml-connector></xml-connector> <xml-gui></xml-gui> </connector>
Please leave xml-connector
and xml-gui
empty.
Congratulations, you have successfully created your own connector.
If you need further information, please do not hesitate open a support ticket.