Forums

New M3 forums on PotatoIT 👍🏻

potatoit's avatarPotato IT

Some of you will have noticed a few changes recently…a new font for the site 🙂

And a new domain…and the purpose of this post – a page which will take you to some development forums (http://forum.potatoit.kiwi).

For quite some time I’ve wanted to create some M3 development specific forums or a mailing list as a nice and convenient way for people to ask quick questions or to share experiences around developing for M3.

I entertained the idea of dropping it on to my server at home, but as you can see from the photo below – home based servers have a tendency to get overwhelmed by dust puppies though mine hasn’t quite gone that far, it’s just a matter of time.  Not to mention that during summer my office gets pretty warm – not exactly lending confidence in the life expectancy of the drives.

Server

I also considered setting…

View original post 119 more words

Custom message process in MEC

Here is an unofficial guide on how to create a custom message process in Infor M3 Enterprise Collaborator (MEC).

What is a message process?

A message process in MEC is one of the steps in a process flow. Technically speaking, it is a Java class that reads a stream of bytes as an input, does some processing on it, and writes a stream of bytes as an output, for example transform a flat file to XML, apply XSLT to an XML, remove an envelope, archive the message, or make a SOAP request. Message processes are chained together in a partner agreement in the Partner Admin Tool.

2_

Documentation

The Partner Admin Tool User Guide has some information about message processes:
1__

Java classes

The message processes are Java classes located in MEC’s core library:

D:\Infor\LifeCycle\host\grid\M3\grids\M3\applications\MECSRVDEV\MecServer\lib\ec-core-x.y.z.jar

Each message process is a Java class in package com.intentia.ec.server.process:4

Each message process may have a configuration dialog box in package com.intentia.ec.partneradmin.swt.agreement: 3

Database

The message processes are declared in the MEC database in table PR_Process:
5

Java code

To create your own message process follow these steps:

  1. Use the following skeleton Java code, fill with your code, set the file extension for the output message (in my example it is .something), use the in input and out output streams to process the message as you need, and eventually use the cat logger to write debug info in the log file:
    package somewhere;
    
    import java.io.InputStream;
    import java.io.OutputStream;
    import org.apache.log4j.Category;
    import com.intentia.ec.server.process.AbstractProcess;
    import com.intentia.ec.server.process.ProcessException;
    
    public class SomeProcess extends AbstractProcess {
    
      private static Category cat = Category.getInstance(SomeProcess.class.getName());
    
      public String getState() {
        return "SomeProcess";
      }
    
      public boolean hasOutput() {
        return true;
      }
    
      public String getFileExtension() {
        return ".something";
      }
    
      public void process(InputStream in, OutputStream out) throws ProcessException {
        // your code here
        cat.info("processing...");
      }
    
    }

    Note: I do not have a sample code for the dialog box, but you can get inspiration from one of the existing classes in package com.intentia.ec.partneradmin.swt.agreement.

  2. Compile the Java code with:
    javac -extdirs D:\Infor\LifeCycle\host\grid\M3\grids\M3\applications\MECSRV\MecServer\lib\ SomeProcess.java
  3. Copy the resulting Java class to the classpath of the MEC server and Partner Admin Tool, in the folder corresponding to the package (in my case it was package somewhere):
    D:\Infor\LifeCycle\host\grid\M3\grids\M3\applications\MECSRV\MecServer\custom\somewhere\SomeProcess.class
    D:\Infor\MECTOOLS\Partner Admin\classes\somewhere\SomeProcess.class

    Note: You can probably also put the Java class in a JAR file; to be tested.

  4. In the MEC database, add the process to the PR_Process table, where ?? is a new ID, for example 27:
    INSERT INTO MECDBDEV.dbo.PR_Process (ID, Name, Description, ConfigurationClass, WorkClass, Standard) VALUES (??, 'Thibaud Process', 'My custom message process', null, 'somewhere.SomeProcess', 1)
  5. In the Infor Grid, restart the MECSRV application to pick up the new Java class:
    6
  6. In the Partner Admin Tool, create a partner agreement and add the message process:
    2
  7. Reload the MEC server to pick up the new agreement:
    7
  8. Run the partner agreement, for example I have a channel detection with a HTTPIn receive channel listening on port 8084, and I make an HTTP request to that port number to trigger the partner agreement.
  9. Check the message received (.rcv) and the message produced (in my case it was extension .something); for that, you will need one Archive process in your partner agreement, before and after your custom process:
    128
  10. You can also open the files directly in the folder specified:
    11
  11. And if you used the logger, you can check your logs in the Event tab:
    10

Real-world example

In my case, I needed a custom message process for the following real-world scenario. My current customer does Procurement PunchOut with its partners using cXML, an old protocol from 1999. In that protocol, there is a step (PunchOutOrderMessage) that sends an XML document in a hidden field cxml-urlencoded of an HTML form. That results in a POST HTTP request (to MEC) with Content-Type: application/x-www-form-urlencoded, with the XML document that is URL-encoded as a value of parameter cxml-urlencoded in the request body. Unfortunately, MEC does not have a message process to extract a specific parameter value of a message, and URL-decode it. So I developed my custom message process as explained above, to take the request body, extract the desired parameter value, URL-decode it, and output the resulting XML. I may write a detailed post about it some day, maybe not.

Conclusion

That was a guide on how to create a custom message process in MEC, doing Java development, to take an input message in a partner agreement, do some custom processing on it, and produce an output message. This is an unofficial solution that I figured out by de-compiling and hacking MEC. There may be a simpler solution, I do not know.

That’s it! Thank you for supporting this blog, please like, subscribe, share around you, and come author the next blog post with us.

Procurement PunchOut with cXML

Hi colleagues. It has been a while since I posted anything. Today I will write a quick post as part of an interface I am currently developing to do procurement PunchOut using cXML, an old protocol from 1999, for my customer and its suppliers. This will eventually end up in their Infor M3 and M3 Enterprise Collaborator implementation.

I only needed to test the Message Authentication Code (MAC) so I wrote a quick prototype in Python.

The cXML User’s Guide describes the MAC algorithm using HMAC-SHA1-96:

Here is my implementation in Python:

# Normalize the values
data = [fromDomain.lower(),
        fromIdentity.strip().lower(),
        senderDomain.lower(),
        senderIdentity.strip().lower(),
        creationDate,
        expirationDate]

# Concatenate the UTF-8-encoded byte representation of the strings, each followed by a null byte (0x00)
data = b''.join([(bytes(x, "utf-8") + b'\x00') for x in data])

# Calculate the Message Authentication Code (MAC)
digest = hmac.new(password.encode("utf-8"), data, hashlib.sha1).digest()

# Truncate to 96 bits (12 bytes)
truncated = digest[0:12]

# Base-64 encode, and convert bytearray to string
mac = str(base64.b64encode(truncated), "utf-8")

# Set the CredentialMac in the XML document
credentialMac = xml.find("Header/Sender/Credential").find("CredentialMac")
credentialMac.attrib["creationDate"] = creationDate
credentialMac.attrib["expirationDate"] = expirationDate
credentialMac.text = mac

Here is my resulting MAC, and it matches that of the cXML User’s Guide, good:

I posted the full source code in my GitHub repository at https://github.com/M3OpenSource/cXML/blob/master/Test.py .

That’s it!

Thank you for continuing to support this blog.

Experimenting with middle-side modifications

With Infor M3, there are server-side modifications, client-side modifications, and the unexplored middle-side modifications. I will experiment with servlet filters for the M3 UI Adapter.

Modification tiers

There are several options to modify M3 functionality:

  • Server-side modifications are M3 Java mods developed with MAK; they propagate to all tiers, including M3 API, but they are often avoided due to the maintenance nightmare during M3 upgrades, and they are banned altogether from Infor CloudSuite multi-tenant. There are also Custom lists made with CMS010 which are great, they are simply configured by power users without requiring any programming, and they survive M3 upgrades.
  • Client-side modifications for Smart Office are Smart Office scripts in JScript.NET, Smart Office SDK features, applications and MForms extensions in C#, Smart Office Mashups in XAML, and Personalizations with wizards. They do not affect M3 upgrades but they apply only to Smart Office. And client-side modifications for H5 Client are H5 Client scripts in JavaScript, and web mashups converted from XAML to HTML5. Likewise, they do not affect M3 upgrades but they apply only to H5 Client.
  • Middle-side modifications are servlet filters for the M3 UI Adapter. They propagate to all user interfaces – Smart Office AND H5 Client – but this is unexplored and perilous. In the old days, IBrix lived in this tier.

M3 UI Adapter

The M3 UI Adapter (MUA), formerly known as M3 Net Extension (MNE), is the J2EE middleware that talks the M3 Business Engine protocol (MEX?) and serves the user interfaces. It was written mostly single-handedly by norpe. It is a simple and elegant architecture that runs As Fast As Fucking Possible (TM) and that is as robust as The Crazy Nastyass Honey Badger [1]. The facade servlet is MvxMCSvt. All the userids/passwords, all the commands, for all interactive programs, for all users, go thru here. It produces an XML response that Smart Office and H5 Client utilize to render the panels. The XML includes the options, the lists, the columns, the rows, the textboxes, the buttons, the positioning, the panel sequence, the keys, the captions, the help, the group boxes, the data, etc.

For example, starting CRS610/B involves:
com.intentia.mc.servlet.MvxMCSvt.doTask()
com.intentia.mc.command.MCCmd.execute()
com.intentia.mc.command.RunCmd.doRunMovexProgram()
com.intentia.mc.engine.ProtocolEngine.startProgram()

The following creates a list with columns:

import com.intentia.mc.metadata.view.ListColumn;
import com.intentia.mc.metadata.view.ListView;

ListView listView = new ListView();
ListColumn listColumn = new ListColumn();
listColumn.setWidth(length);
listColumn.setConstraints(constraints);
listColumn.setCaption(new Caption());
listColumn.setConditionType(1);
listColumn.setHeader(headerSplitterAttr);
listColumn.setName("col" + Integer.toString(columnCount));
listColumn.setJustification(1);
listColumns.add(listColumn);
listView.addFilterField(posField, listColumn);
listView.setListColumns((ListColumn[])listColumns.toArray(new ListColumn[0]));

Here is an excerpt of the XML response for CRS610/B that shows the list columns and a row of data:
Fiddler

Experiment

This experiment involves adding a servlet filter to MvxMCSvt to transform the XML response. Unfortunately, MNE is a one-way function that produces XML in a StringBuffer, but that cannot conversely parse the XML back into its data structures. Thus, we have to transform the XML ourselves. I will not make any technical recommendations for this because it is an experiment. You can refer to the existing MNE filters for examples on how to use the XML Pull Parser (xpp3-1.1.3.4.O.jar) that is included in MNE. And you can use com.intentia.mc.util.CstXMLNames for the XML tag names.

To create a servlet filter:

/*
D:\Infor\LifeCycle\host\grid\XYZ\runtimes\1.11.47\resources\servlet-api-2.5.jar
D:\Infor\LifeCycle\host\grid\XYZ\grids\XYZ\applications\M3_UI_Adapter\lib\mne-app-10.2.1.0.jar
javac -cp servlet-api-2.5.jar;mne-app-10.2.1.0.jar TestFilter.java
*/

package net.company.your;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import com.intentia.mc.util.Logger;

public class TestFilter implements Filter {

    private static final Logger logger = Logger.getLogger(TestFilter.class);

    public void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Hello, World");
        }
        chain.doFilter(request, response);
    }

    public void destroy() {}

}

Add the servlet filter to the MNE deployment descriptor at D:\Infor\LifeCycle\host\grid\XYZ\grids\XYZ\applications\M3_UI_Adapter\webapps\mne\WEB-INF\web.xml:

<filter>
    <filter-name>TestFilter</filter-name>
    <filter-class>net.company.your.TestFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>TestFilter</filter-name>
    <servlet-name>MvxMCSvt</servlet-name>
</filter-mapping>

Then, reload the M3UIAdapterModule in the Infor Grid. This will destroy the UI sessions, and users will have to logout and logon M3 again.

Optionally, set the log level of the servlet filter to DEBUG.

Limitations

MvxMCSvt is the single point of entry. If you fuck it up, it will affect all users, all programs, on all user interfaces. So this experiment is currently a Frankenstein idea that would require a valid business case, a solid design, and great software developers to make it into production.

Also, changes to the web.xml file will be overriden with a next software update.

Discussion

Is this idea worth pursuing? Is this another crazy idea? What do you think?

Infor launches Infor M3 13.3

Infor announced that M3 13.3 is now generally available. Noticeable for me are:

  • Documentation available online at docs.infor.com
  • SSH FTP (SFTP) channel available for M3 Enterprise Collaborator (MEC)
  • New richer F4-browse selectors
  • M3 now available on Red Hat Enterprise Linux and PostgreSQL
  • New M3 API available to query Custom Lists (CMS015)
  • New techniques to avoid modifications

For more details, visit the Infor M3 page on the Infor Sales Portal.

Application messages in Infor Smart Office

Here is an illustration of application messaging in Infor Smart Office to send, broadcast, and receive messages, and process responses between applications in Smart Office, whether in scripts, Mashups, or other entities.

Documentation

The Infor Smart Office SDK Developer’s Guide has Chapter 19 Application messages, and the Smart Office SDK help file has the API reference for Mango.UI.Services.Messages.ApplicationMessageService:
3

Note: For more information on the Smart Office SDK refer to my previous post.

Scripts

Here are some examples of sending, broadcasting and receiving messages, and processing responses in Smart Office scripts; the other party in the communication can be another script, a Mashup or another entity in Smart Office.

To send a message and process the response:

import Mango.UI.Services.Messages;

package MForms.JScript {
	class ApplicationA {
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			// send message
			var message: ApplicationMessage = new ApplicationMessage();
			message.Sender = "ApplicationA";
			message.Recipient = "ApplicationB";
			message.Parameter = "Hello World";
			var response: ApplicationMessageResponse = ApplicationMessageService.Current.SendMessage(message);
			// process response
			response.MessageStatus;
			response.Result;
		}
	}
}

To receive a message and return a response:

import Mango.UI.Services.Messages;

package MForms.JScript {
	class ApplicationB {
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			ApplicationMessageService.Current.AddRecipient("ApplicationB", OnMessage);
		}
		function OnMessage(message: ApplicationMessage): ApplicationMessageResponse {
			message.Sender;
			message.Recipient;
			message.Parameter;
			var response: ApplicationMessageResponse = new ApplicationMessageResponse();
			response.MessageStatus = MessageStatus.OK;
			response.Result = "Bonjour";
			return response;
		}
	}
}

To broadcast a message (there is no response):

import Mango.UI.Services.Messages;

package MForms.JScript {
	class ApplicationC {
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			var broadcastMessage: ApplicationMessage = new ApplicationMessage();
			broadcastMessage.Sender = "ApplicationC";
			broadcastMessage.Recipient = "GroupX";
			broadcastMessage.Parameter = "HELLO WRRRLD!!!!!!";
			ApplicationMessageService.Current.SendBroadcastMessage(broadcastMessage);
		}
	}
}

To receive a broadcasted message (there is no response):

import Mango.UI.Services.Messages;

package MForms.JScript {
	class ApplicationD {
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			ApplicationMessageService.Current.AddBroadcastRecipient("GroupX", OnBroadcastMessage);
		}
		function OnBroadcastMessage(message: ApplicationMessage) {
			message.Sender;
			message.Recipient;
			message.Parameter;
		}
	}
}

Mashups

Here are some examples of sending, broadcasting and receiving messages in Smart Office Mashups (there are no responses); the other party in the communication can be another Mashup, a script or another entity in Smart Office.

To send a message (there is no response):

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:Mango.UI.Controls;assembly=Mango.UI" xmlns:mashup="clr-namespace:Mango.UI.Services.Mashup;assembly=Mango.UI">
	<Button Name="BtnMessage" Content="Send" Width="150" />
	<mashup:ApplicationMessageControl Name="test">
		<mashup:ApplicationMessageControl.Events>
			<mashup:Events>
				<mashup:Event SourceName="BtnMessage" SourceEventName="Click" TargetEventName="Send" Debug="True">
					<mashup:Parameter TargetKey="Sender" Value="MashupE" />
					<mashup:Parameter TargetKey="Recipient" Value="MashupF" />
					<mashup:Parameter TargetKey="Parameter" Value="Hello World" />
				</mashup:Event>
			</mashup:Events>
		</mashup:ApplicationMessageControl.Events>
	</mashup:ApplicationMessageControl>
</Grid>

To receive a message (there is no response):

 <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:Mango.UI.Controls;assembly=Mango.UI" xmlns:mashup="clr-namespace:Mango.UI.Services.Mashup;assembly=Mango.UI">
	<mashup:ApplicationMessageControl Name="test" Recipient="MashupF">
		<mashup:ApplicationMessageControl.Events>
			<mashup:Events>
				<mashup:Event SourceEventName="Received" Debug="True">
					<mashup:Parameter SourceKey="Sender" />
					<mashup:Parameter SourceKey="Recipient" />
					<mashup:Parameter SourceKey="Parameter" />
				</mashup:Event>
			</mashup:Events>
		</mashup:ApplicationMessageControl.Events>
	</mashup:ApplicationMessageControl>
</Grid>

To broadcast a message:

<mashup:Event...TargetEventName="Broadcast">

To receive a broadcasted message:

<mashup:ApplicationMessageControl...BroadcastRecipient="Everyone">

Illustration

Here is an illustration of messages going in all directions between scripts and Mashups:
r

More

The ApplicationMessageService API has more methods and properties available:

  • You can send multiple parameters and multiple result values with Dictionary<string, Object>.
  • You can return different MessageStatus values depending on your needs, for example OK, Failed or InvalidMessage.
  • You can send a message asynchronously to not block the UI; but there is no response.
  • You should verify if the recipient already exists before adding a message handler; otherwise you will get the exception “A recipient with the key has already been added.”
  • You can remove recipients.

Refer to the SDK documentation for more information.

That’s it. Please like, comment, share, follow, contribute.

Integrating Zeacom call center with Infor Smart Office

Here is a source code that a customer and I worked out to integrate the Zeacom call center with Infor Smart Office such that their customer service representatives can receive phone calls from their customers and automatically launch the respective M3 customer programs; this is similar to the previous integration work with Cisco Agent Desktop, Twilio, Skype, etc.

This source code is a script assembly in C#; for more information on script assemblies see here and here. The trick was to keep the z variable as a global variable, not as a local variable, so it can survive in memory for the event handlers.

You will need to reference the DLL files from your Zeacom software.

using Mango.UI;
using MForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace MForms.JScript
{
	public class ZeacomAssembly
	{
		MLINTERFACELib.ZeacomMLI z = new MLINTERFACELib.ZeacomMLI();

		public void Init(object element, object args, object controller, object debug)
		{
			string phoneExt = "1234";
			if (z.AddExtension(phoneExt))
				if (z.Initialise())
					z.OnNewExtensionCall += OnNewExtensionCall;
		}

		private void OnNewExtensionCall(object Extension, object Call)
		{
			Application.Current.Dispatcher.Invoke(new ShowInformationDelegate(ShowInformation), new object[] {Extension, Call});
		}

		private delegate void ShowInformationDelegate(object Extension, object Call);

		private void ShowInformation(object Extension, object Call)
		{
			ConfirmDialog.ShowInformationDialog("Incoming Call", "MLI Event: OnNewExtensionCall(: " + ((MLINTERFACELib.IExtension)Extension).AddressName + ", " + ((MLINTERFACELib.ICall)Call).CallReference + OutputCallInfo((MLINTERFACELib.ICall)Call));
		}
		private string OutputCallInfo(MLINTERFACELib.ICall Call)
		{
			StringBuilder CallString = new StringBuilder();
			if (Call != null)
			{
				CallString.AppendLine("CalledID = " + Call.CalledId);
				CallString.AppendLine("Caller = " + Call.Caller);
				CallString.AppendLine("CallOrigin = 0x" + Call.CallOrigin.ToString("X"));
				CallString.AppendLine("CallReason = 0x" + Call.CallReason.ToString("X"));
				CallString.AppendLine("CallReference = 0x" + Call.CallReference.ToString("X"));
				CallString.AppendLine("CLI = " + Call.CLI);
				CallString.AppendLine("CLIA = " + Call.CLIA);
				CallString.AppendLine("CLIF = " + Call.CLIF);
				CallString.AppendLine("CLIP = " + Call.CLIP);
				CallString.AppendLine("DNIS = " + Call.DNIS);
				CallString.AppendLine("Pilot = " + Call.Pilot);
				CallString.AppendLine("Query = " + Call.Query);
				CallString.AppendLine("QueryName = " + Call.QueryName);
				CallString.AppendLine("Queue = " + Call.Queue);
				CallString.AppendLine("RelatedRef = 0x" + Call.RelatedRef.ToString("X"));
				CallString.AppendLine("TransferredID = " + Call.TransferredId);
				CallString.AppendLine("TransferrerID = " + Call.TransferrerId);
				CallString.AppendLine("Trunk = " + Call.Trunk);
				CallString.AppendLine("Type = " + Call.Type);
				CallString.AppendLine("Wait = " + Call.Wait);

				if (Call.DataCallPayloadString.Length > 0)
					CallString.AppendLine("DataCallPayloadString = '" + Call.DataCallPayloadString + "'");

				CallString.AppendLine("");
			}
			return CallString.ToString();
		}
	}
}

Call M3 API from Event Analytics rules

Here is how to call M3 API from a Drools rule in Infor Event Analytics; this is a common requirement.

Sample scenario

Here is my sample business case.

When a user changes the status of an approval line in OIS115 (OOAPRO), I have to find the order type (ORTP) of the order to determine which approval flow to trigger in Infor Process Automation (IPA), but ORTP is not part of the table OOAPRO, for that reason I must previously make a call to OIS100MI.GetHead.

I could call M3 in the approval flow, but false positives would generate noise in the WorkUnits.

Is it possible?

I asked Nichlas Karlsson, Senior Architect – Business Integration at Infor, if it was possible to call M3 API directly in the Drools rule. He is one of the original developers of Event Hub and Event Analytics and very helpful with my projects (thank you) although he does not work with these products any longer. He responded that Event Analytics is a generic software with no specific connection to M3, so unfortunately this is not possible out of the box, however it is a common requirement. He said I could solve it using MvxSockJ to call M3 APIs in my own Java class, included in a jar that I put in the lib folder. He added to not forget that the execution time for all rules within a session must be less than the proxy timeout, i.e. 30s. And I would also need to manage host, port, user, password and other properties in some way.

Instead of MvxSockJ I will use the MI-WS proxy of the Grid as illustrated in my previous post.

Sample Drools rule

Here is my sample Drools rule that works:

package com.lawson.eventhub.analytics.drools;

import java.util.List;
import com.lawson.eventhub.analytics.drools.model.Event;
import com.lawson.eventhub.analytics.drools.model.HubEvent;
import com.lawson.eventhub.EventOperation;
import com.lawson.grid.node.Node;
import com.lawson.grid.proxy.access.SessionId;
import com.lawson.grid.proxy.access.SessionProvider;
import com.lawson.grid.proxy.access.SessionUtils;
import com.lawson.grid.proxy.ProxyClient;
import com.lawson.grid.registry.Registry;
import com.lawson.miws.api.data.MIParameters;
import com.lawson.miws.api.data.MIRecord;
import com.lawson.miws.api.data.MIResult;
import com.lawson.miws.api.data.NameValue;
import com.lawson.miws.proxy.MIAccessProxy;

declare HubEvent
	@typesafe(false)
end

rule "TestSubscription"
	@subscription(M3:OOAPRO:U)
	then
end

rule "TestRule"
	no-loop
	when
		event: HubEvent(publisher == "M3", documentName == "OOAPRO", operation == EventOperation.UPDATE, elementOldValues["STAT"] == 10, elementValues["STAT"] == "20")
	then
		// connect to MI-WS
		Registry registry = Node.getRegistry();
		SessionUtils su = SessionUtils.getInstance(registry);
		SessionProvider sp = su.getProvider(SessionProvider.TYPE_USER_PASSWORD);
		SessionId sid = sp.logon("Thibaud", "******".toCharArray());
		MIAccessProxy proxy = (MIAccessProxy)registry.getProxy(MIAccessProxy.class);
		ProxyClient.setSessionId(proxy, sid);

		// prepare input parameters
		MIParameters p = new MIParameters();
		p.setProgram("OIS100MI");
		p.setTransaction("GetHead");
		MIRecord r = new MIRecord();
		r.add("CONO", event.getElementValue("CONO"));
		r.add("ORNO", event.getElementValue("ORNO"));
		p.setParameters(r);

		// execute and get output
		MIResult s = proxy.execute(p);
		List<MIRecord> records = s.getResult(); // all records
		if (records.isEmpty()) return;
		MIRecord record = records.get(0); // zeroth record
		List<NameValue> nameValues = record.getValues(); // all output parameters
		String ORTP = nameValues.get(4).getValue(); // PROBLEM: somehow nameValues.indexOf("X") returns -1

		// make decision
		if (ORTP.equals("100")) event.postEvent("ApprovalFlowA");
		if (ORTP.equals("200")) event.postEvent("ApprovalFlowB");
		if (ORTP.equals("300")) event.postEvent("ApprovalFlowC");
end

Note: You will need to drop foundation-client-10.1.1.3.0.jar in the lib folder of Event Analytics, and restart the application

Limitations

There are some limitations with this code:

  • The execution time must be less than the 30s proxy timeout
  • Limit the number of return columns; there is currently a bug with Serializable in ColumnList, see Infor Xtreme incident 8629267
  • If the M3 API returns an error message it will throw the bug with Serializable in MITransactionException, see Infor Xtreme incident 8629267
  • Somehow NameValue.indexOf(name) always returned -1 during my tests, it is probably a bug in the class, so I had to hard-code the index value of the output field (yikes)
  • I do not know how to avoid the logon to M3 with user and password to get a SessionId; I wish there was a generic SYSTEM account that Event Analytics could use
  • For simplicity of illustration I did not verify all the null pointers; you should do the proper verifications
  • The code may throw MITransactionException, ProxyException and IndexOutOfBoundsException
  • You can move the Java code to a separate class in the lib folder; for that refer to my previous post

Related articles

That’s it. Let me know what you think in the comments below.

How to call M3 API from the Grid application proxy

Here is how to call M3 API using the MI-WS application proxy of the Infor Grid.

This is useful if we want to benefit from what is already setup in the Grid and not have to deal with creating our own connection to the M3 API server with Java library, hostname, port number, userid, password, connection pool, etc.

Note: For details on what Grid application proxies are, refer to the previous post.

MI-WS application proxy

The MI-WS application is part of the M3 Business Engine Foundation. We will need foundation-client.jar to compile our classes:
1b

Step 1. Logon to the Grid

First, login to the Grid from your application and get a SessionId and optionally a GridPrincipal.

From a Grid application:

import com.lawson.grid.proxy.access.GridPrincipal;
import com.lawson.grid.proxy.access.SessionController;
import com.lawson.grid.proxy.access.SessionId;

// get session id
SessionId sid = ??? // PENDING
GridPrincipal principal = ??? // PENDING;

From a client application outside the Grid:

import com.lawson.grid.proxy.access.GridPrincipal;
import com.lawson.grid.proxy.access.SessionId;
import com.lawson.grid.proxy.access.SessionProvider;
import com.lawson.grid.proxy.access.SessionUtils;
import com.lawson.grid.proxy.ProxyException;

// logon and get session id
SessionUtils su = SessionUtils.getInstance(registry);
SessionProvider sp = su.getProvider(SessionProvider.TYPE_USER_PASSWORD);
SessionId sid;
try {
    sid = sp.logon(userid, password.toCharArray());
} catch (ProxyException e) {
    ...
}
GridPrincipal principal = su.getPrincipal(sid);

Step 2. Call the M3 API

Second, call the M3 API, for example CRS610MI.LstByNumber, and get the result:

import java.util.ArrayList;
import java.util.List;
import com.lawson.grid.proxy.ProxyClient;
import com.lawson.grid.proxy.ProxyException;
import com.lawson.miws.api.data.MIParameters;
import com.lawson.miws.api.data.MIParameters.ColumnList;
import com.lawson.miws.api.data.MIRecord;
import com.lawson.miws.api.data.MIResult;
import com.lawson.miws.api.MITransactionException;
import com.lawson.miws.proxy.MIAccessProxy;

// get the proxy
MIAccessProxy proxy = (MIAccessProxy)registry.getProxy(MIAccessProxy.class);

// login to M3
ProxyClient.setSessionId(proxy, sid);

// prepare the parameters
MIParameters paramMIParameters = new MIParameters();
paramMIParameters.setProgram("CRS610MI");
paramMIParameters.setTransaction("LstByNumber");
paramMIParameters.setMaxReturnedRecords(10);

// set the return columns
ColumnList returnColumns = new ColumnList();
List<String> returnColumnNames = new ArrayList<String>();
returnColumnNames.add("CONO");
returnColumnNames.add("CUNO");
returnColumnNames.add("CUNM");
returnColumns.setReturnColumnNames(returnColumnNames);
paramMIParameters.setReturnColumns(returnColumns);

// execute
MIResult result;
try {
	result = proxy.execute(paramMIParameters);
} catch (MITransactionException e) {
	...
} catch (ProxyException e) {
	...
}

// show the result
List<MIRecord> records = result.getResult();
for (MIRecord record: records) {
	record.toString();
}

Note: When I use ColumnList it throws java.io.NotSerializableException: com.lawson.miws.api.data.MIParameters$ColumnList. It appears to be a bug in that the ColumnList class is missing implements Serializable. I reported it in Infor Xtreme incident 8629267.

That’s it. Please let me know what you think in the comments below.

Application proxies in the Infor Grid

Here is how to create and invoke application proxies of the Infor Grid.

What is an application proxy?

An application proxy is an interface for a Grid application that other applications of the Grid or client applications outside the Grid can invoke; it is inter-process communication in a distributed environment.

Example. The proxy for Grid application C is invoked from grid application A of a different host, from grid application D of the same host, and from client application K outside the Grid.

How to create an application proxy?

To create a proxy for a Grid application:

package net.company.your;

import com.lawson.grid.node.application.ApplicationEntryPoint;
import com.lawson.grid.node.application.ModuleContext;
import com.lawson.grid.proxy.ProxyException;
import com.lawson.grid.proxy.ProxyServer;

public interface HelloWorldProxy {
    public String hello(String name) throws ProxyException;
}

public class HelloWorld implements HelloWorldProxy {
    public String hello(String name) {
        return "Hello, " + name;
    }
}

public class HelloWorldApp implements ApplicationEntryPoint {
    public boolean startModule(ModuleContext context) {
        ProxyServer.registerProxy(HelloWorldProxy.class, new HelloWorld());
        return true;
    }
    public void stopModule() {
    }
}

Note: For details on how to create a Grid application, refer to the previous post on the subject.

The proxy is now ready to be invoked:
8

How to call the proxy from a Grid application?

To invoke the proxy from a Grid application:

The short source code is:

Registry registry = Node.getRegistry();
HelloWorldProxy proxy = registry.getProxy(HelloWorldProxy.class);
String result = proxy.hello("Thibaud");

The long source code is:

package net.company2.your;

import com.lawson.grid.node.application.ApplicationEntryPoint;
import com.lawson.grid.node.application.ModuleContext;
import com.lawson.grid.node.Node;
import com.lawson.grid.proxy.ProxyException;
import com.lawson.grid.registry.Registry;
import net.company.your.HelloWorldProxy;

public class HelloWorldApp2 implements ApplicationEntryPoint {
	public boolean startModule(ModuleContext context) {
		// connect
		Registry registry = Node.getRegistry();
		if (!registry.isConnected()) {
			System.err.println("failed to connect");
			return false;
		} else {
			System.out.println("connected to " + registry.getGridName());
		}
		// get the proxy
		HelloWorldProxy proxy = (HelloWorldProxy)registry.getProxy(HelloWorldProxy.class);
		if (proxy == null) {
			System.err.println("failed to get proxy...exiting");
			return false;
		} else {
			System.out.println("got proxy");
		}
		// invoke the proxy
		try {
			String result = proxy.hello("Thibaud");
			System.out.println(result);
		} catch (ProxyException e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
			return false;
		}
		return true;
	}

	public void stopModule() {
	}

}

How to call the proxy from a client application?

To invoke the application proxy from a client application outside the Grid:

First, choose a Grid registry’s hostname and port number (a registry is a special node that keeps track of all nodes in a Grid):

Then, connect to the Grid, get the proxy and invoke its methods.

The short source code is:

ClientRegistry registry = new ClientRegistry("Test");
registry.connect(hostname, portNumber);
HelloWorldProxy proxy = registry.getProxy(HelloWorldProxy.class);
String result = proxy.hello("Thibaud");

The long source code is:

import com.lawson.grid.proxy.ProxyConnectionFailedException;
import com.lawson.grid.proxy.ProxyException;
import com.lawson.grid.proxy.access.GridPrincipal;
import com.lawson.grid.proxy.access.SessionId;
import com.lawson.grid.proxy.access.SessionProvider;
import com.lawson.grid.proxy.access.SessionUtils;
import com.lawson.grid.registry.ClientRegistry;
import net.company.your.HelloWorldProxy;

public class Test {
    public static void main(String[] args) {

        // connect
        ClientRegistry registry = new ClientRegistry("Test");
        try {
            registry.connect("host1623", 22102);
        } catch (ProxyConnectionFailedException e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
            return;
        }
        if (!registry.isConnected()) {
            System.err.println("failed to connect");
            return;
        } else {
            System.out.println("connected to " + registry.getGridName());
        }

        // get the proxy
        HelloWorldProxy proxy = (HelloWorldProxy)registry.getProxy(HelloWorldProxy.class);
        if (proxy == null) {
            System.err.println("failed to get proxy...exiting");
            return;
        } else {
            System.out.println("got proxy");
        }

        // invoke the proxy
        try {
            String result = proxy.hello("Thibaud");
            System.out.println(result);
        } catch (ProxyException e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
            return;
        }

        // disconnect
        registry.close();
        if (!registry.isClosed()) {
            System.err.println("failed to close");
        } else {
            System.out.println("registry closed");
        }
    }

}

That’s it. Please like, subscribe, share.