Developing Plug-Ins - Process
This How-To is brought to you by Jan Thielemann from evenos GmbH (www.evenos-consulting.de). If you have questions, criticism or improvement suggestions, feel free to visit me (Jan.thielemann) or write me an email
Goal of this how-to
The goal of this how-to is to show you, how you would develop a new process within a plug-in. Also we will discover some useful information about how to use iDempiere Processes properly.
What you will learn:
- Create and run a new process which is located in your custom plug-in project
- Get the process parameters from within your process
- Log useful information while the process is running
- How to handle errors which properly during the process is running and after it is finished
Prerequisites
Before you start developing Processes in your plug-ins, you should take a look at Developing Plug-Ins without affecting trunk and Get your Plug-In started to get a good starting point.
tl;dr
https://www.youtube.com/watch?v=5SDqnbYrpQc
The workflow
Creating the process class
After creating the plug-in, the first thing you want to do is creating a new class for your process. In our case, we simply call it MyProcess. This class inherits from SvrProcess and implements the necessary methods. This is where you write all the logic you want the process to handle.
Once the process class is created, you need to tell iDempiere how to find it. This is done one of the following approaches:
Since iDempiere 9: New Process Factory and Annotation
If you're running iDempiere 9 or higher, you can use the new process factory and annotations approach. You can find how to do it here: NF9 OSGi New Process Factory.
Using a Process Factory
Using a process factory is the default way to provide processes for iDempiere. Instead of using extension points in your manifest, you use component definition which makes use of the IProcessFactory service. To do this, first create a new class for your factory. Call it for example MyProcessFactory.java. In this class, implement the IProcessFactory interface and its methods. In newProcessInstance(String), create a if-statement where you check for your class name and if it fits, return a new instance of your process.
Then create a new component definition, you can do it via annotations following this guide.
If you want to do it manually, start by clicking File>New>Others>Component Definition. Give it a unique name and click on finish. After it is created, make sure that the corresponding entry is created in your MANIFEST.MF file. I like to put all my component definitions in a folder called OSGI-INF and only add this folder to my manifest:
Export-Package: ... Service-Component: OSGI-INF/*.xml Bundle-ClassPath: ... Bundle-Activator: ...
Afterwards, open your component definition and make sure that the name is unique in idempiere. It's a good idea to use your reverse domain name followed by the class or plugin name. Next, select the class which implements the process factory interface and add a new property called service.ranking which is a integer. Give it a value of 100 or so. This is needed, so it is loaded before the default process factory from idempiere. Go to the services tab and add the IProcessFactory to the provided services section. Save it and you are done.
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.evenos.pluginname.process.factory"> <implementation class="org.evenos.packagename.MyProcessFactory"/> <property name="service.ranking" type="Integer" value="100"/> <service> <provide interface="org.adempiere.base.IProcessFactory"/> </service> </scr:component>
Using Extension Points
This is not recommended anymore, since the two approaches mentioned above are a better practice. However, if you're running an old version of iDempiere you can add custom plug-ins using extension points. Open your MANIFEST.MF and switch to the Extensions tab. Add a org.adempiere.base.Process extension. Right-click it and add a new process. Click the Browse button and select MyProcess. Copy the full class name and paste it into the org.adempiere.base.Process name and id field:
The plugin XML should look something like this:
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension id="org.notima.idempiere.process.CreateWarehouseLocators" name="org.notima.idempiere.process.CreateWarehouseLocators" point="org.adempiere.base.Process"> <process class="org.notima.idempiere.process.CreateWarehouseLocators"> </process> </extension> </plugin>
NOTE! Eclipse doesn't automatically add the "process" tag and without it your process' class won't be found.
Sart the iDempiere Client and log in as SuperUser/System. Open the Report & Process window and create a new entry. We call it MyProcess. As the class name, insert the fully qualified classname (in our case org.evenos.process.MyProcess):
Switch to the Parameter tab and add some parameters. For testing purpose we will add the follwoing parameters: -M_Product_ID - Table Direct - DBColumn "M_Product_ID" -SomeString - String - DBColumn "SomeString" -SomeInteger - Integer - DBColumn "SomeInteger"
Open the Menu window and create a new entry. Name it MyProcess and chose Process as the action. In the Process field, select your previously created process:
Log out and back in. You should see your Process in the menu. If you have done everything right, you can start it and it will return "** null" as the message. If any error occurs, make sure you followed our previous tutorials. Check if your plug-in is started and active and if you have chosen the correct class names.
Good to know
Reading Parameters
Go back to Eclipse and open MyProcess, it's time to add some more functionality here. First we want to read the parameters for this process. Start by adding the instance variables. Then go to prepare() and add the code to read in the parameters. It should now look like this (except the typo in M_Product_ID ;):
Log information from within the process
There are different ways of logging which are commonly used in iDempiere. In a process, you have two, respectively three options we want to show you. With the first option, you should be very familiar. Its the logging via a CLogger. Every process which inherits from SvrProcess has a instance variable called "log". You can use this logger to log information like in any other place where you use this kind of logger. The logs will show up on the console and the iDempiere logs if configured. If you deactivate iDempieres logging functionality you won't see these logs. We already made a call to the default logger in our prepare() method.
The next logging technique is calling one of the addLog() methods. These logs will show up in the Process Audit window in iDempiere. Imagine your process does some kind of batch processing. Maybe there can go something wrong. Instead of canceling the whole process if an error occurs, you could simply add a special log entry to the Process Audit. The addLog() methods can be simple and just log a string or they can even link to a Table/Record so you can jump right to the corresponding entrie from within the Process Audit. Lets add some logs. One which logs the input String and one which leads us to the product which we can select as the parameter:
addLog(getProcessInfo().getAD_Process_ID(), new Timestamp(System.currentTimeMillis()), new BigDecimal(getProcessInfo().getAD_PInstance_ID()), "The input string was: " + someString); addLog(getProcessInfo().getAD_Process_ID(), new Timestamp(System.currentTimeMillis()), new BigDecimal(getProcessInfo().getAD_PInstance_ID()), "The input integer was: " + someInteger, MProduct.Table_ID, m_product_id);
There is more you can and should log. Have you noticed that doIt() can throw exceptions and return a String? The return statement is shown to the user. Remember the "** null" message when we fist started the process? You can decide what you want to return. If you want to throw an error, you can do this. The current running process will then stop and show the localized error description instead of your return statement. If you check the Process Audit window, you can see if a process was canceled because of an error by its Result value. 0 means that an exception occured, 1 means that you returned your own value. Lets add a statement to make it possible to throw errors:
Start the iDempiere client and try out the new process. Log in as GardenAdmin / GardenWorld so you have some products available. Start the process with the following parameters:
The result should look like this:
Now open the Process Audit window and find your process. It should look like this:
On the Log tab in the Process Audit window, you should see our three logs. Select the last one and switch to the single row layout. Click the Record ID button. It will lead you directly to the selected Product.
The last thing you can do now is, start the process again but this time, chose an integer value > 10. Compare the two runs in the Process Audit window:
Interact with the User
Some times, you need to interact with the user during a process run to make for example a decissions. Why not giving the process these decissions as parameters before the run, you will ask? Because you might not know the questions you would have to answer. Let me give you this example: In europe, we have SEPA (Single European Payment Area) which is a united money payment method across the whole eu. Now if you want to send a payment via online banking, you will send your bank account numbers and stuff to the banking server. The server then generates some numbers which you have to enter into a small device which then generates the transactionnumber for this payment. This number will be sent to the bank again. You see, during the process of sending a payment, you need to interact with the user.
iDempiere offers you a easy way to interact with the user during a process run. All you have to do is, to call the "processUI" instance variable, call one of the ask()-methods and give it a callback. This is what youe doIt()-method could look like with a simple ask-panel for a boolean input:
@Override protected String doIt() throws Exception { this.processUI.ask("Yes or No?", new Callback<Boolean>() { public void onCallback(Boolean result) { addLog("You selected " + (result ? "Yes" : "No")); } }); return null; }
Another way to interact with the user, is to ask him for a input string. If not already integrated into the trunk, you have to install the patch provided here to get this working. Once installed, you can ask for the user input by using:
processUI.askForInput("Please enter some String", new Callback<String>() { @Override public void onCallback(String result) { addLog("You entered: " + result); } });
Now the problem here is, that you process will finish running before you have the answer. To avoid this, we need to block the process till you get the anser. The easiest way to do this is by using a StringBuffer because we need a final variable which we can pass to the inner class. Heres how you can implement the blocking:
final StringBuffer answer = new StringBuffer(); final StringBuffer retVal = new StringBuffer(); processUI.askForInput("Please enter some String", new Callback<String>() { @Override public void onCallback(String result) { addLog("You entered: " + result); retVal.append(result); answer.append("done"); } });
while (answer.length() == 0) { try { Thread.sleep(200); } catch (InterruptedException e) {} }
The reason why we use two separate StringBuffers here is that the result could be empty so you process would be stuck forever. If you ask for a boolean, you can simply append the boolean to the StringBuffer and you don't need a second one.