PDF Merger – a starting point

One of our customer service’s (CS) requirement was to be able to retrieve from M3 a combined .pdf file containing the 3 main documents they usually send to the forwarding agent :

  • CMR (MWS610PF) : used for customs, international shipments
  • Customer invoice (OIS199PF)
  • Packing list (MMS480PF)

This requirement is not possible to meet in standard, even with Infor Document Management (IDM) installed within our M3 environment.

At the time being, CS team must :

  • retrieve one by one each correct .pdf file (we have duplicates on CMR… to solve)
  • print each document
  • scan the batch of files and send it to their personal mailbox
  • forward it to the proper “mail to”

The idea here is to offer the possibility to the final user to easily trigger “on demand” the combined .pdf files for one delivery index and receive it automatically in their mailbox.


This post is a starting point and present what can be done. The process can certainly be enhanced, so feel free to comment.


Technical choice made :

  • create a widget in Smart Office : allow document selection and delivery number entry
  • check data entered, return error if needed
  • trigger a MEC mapping that will fetch the requested files through a XQuery in IDM database (that’s where the IDM API play a role) + merge the files + send the result by email with attachment

note : MEC has been used because i actually don’t know well how to use an assembly in .NET but i am sure we can build a java program and call it directly from the script.


IDM – Infor Document Managment

In IDM, you can XQuery the database, asking for different documents tied to a given delivery number for example, but you can only select one line at a time. And there is no merge possibility at the time being.

Infor provides useful java APIs for IDM, already discussed in Thibaud’s post https://m3ideas.org/tag/document-archive/


The useful one is icp.jar that you can decompile :


The widget

Inspired by another post from Thibaud (https://m3ideas.org/2012/05/08/stand-alone-scripts-for-smart-office/), we create a shortcut on the canvas that will launch the script behind.


The code is pretty straightforward :

var task = new Task(new Uri('jscript://'));
task.AllowAsShortcut = false;
task.VisibleName = 'PDF Merger';
var runner =DashboardTaskService.Current.LaunchTask(task,HostType.Widget);
runner.Status = RunnerStatus.Running;
var host = runner.Host;
host.HostContent = CreateWindow();

host.HostTitle = 'PDF Merger';
host.ResizeMode = ResizeMode.NoResize;
host.Width = 370;
host.Height = 400;

The CreateWindow() function contains a WrapPanel with buttons, labels, textbox and a StatusBar to show some message if needed (it helped me debug).


The GO button has OnClick event.

//Button GO
var butGO= new Button();
butGO.Margin = new Thickness(10, 0, 10, 0);
butGO.Content = "GO !";
butGO.Width = "40";
butGO.Background = new SolidColorBrush(Colors.Blue);
butGO.Foreground = new SolidColorBrush(Colors.White);


We monitor at least that :
– the delivery number exist in the system (MIWorker MWS410MI/GetHead)
– the email address of the user exist in CRS111 type 04 (MIWorker CRS111MI/Get)
– the user has selected at least one document (check property IsChecked on checkboxes)

public function OnClickbutGO(sender: Object, e: RoutedEventArgs) 
   if(cb1.IsChecked == false && cb2.IsChecked == false && cb3.IsChecked == false)
           ConfirmDialog.ShowErrorDialogWithoutCancel("Select at least one document.");     
   else if(EMAIL == '')
        ConfirmDialog.ShowErrorDialogWithoutCancel("Email address missing in M3.","Check CRS111 type 04 and relaunch the widget.");
   else if(dlixBox.Text != '')
           //check DLIX value
          var record = new MIRecord();
          record["CONO"] = wCONO;
          record["DLIX"] = dlixBox.Text;
          MIWorker.Run("MWS410MI", "GetHead", record, OnRunCompletedMWS410MI);

         catch (ex)
          debug.WriteLine("ex : "+ex);
           ConfirmDialog.ShowErrorDialogWithoutCancel("DLIX number is mandatory.");

… and if everything is OK then we trigger the MEC mapping by dropping an XML file into the proper directory.


The XML file is built by using  the XmlWriter (System.Xml).

I chose a classic MBM initiator detection in MEC (MBM_S2_R1) and built the global MBM intiator.

This kind of file can be dropped into the input directory. It’s also possible to go for a channel detection where you can drop a more light xml file with the minimum of tags; in this case, you can drop all the tags used for detection by the partner administrator.

Minimum mandatory data to transfer to MEC is :

  • the XQuery : tag “DocumentVariant”
  • the email address of the user (extracted from CRS111MI) : tag “DocumentNumber”


You can change the tags at your convenience!

function buildXMLTrigger() 
    var request : String = "(";
    if(cb1.IsChecked == true)  request = request + "/CMR|";
    if(cb2.IsChecked == true)  request = request + "/CustomerInvoice|";
    if(cb3.IsChecked == true)  request = request + "/DeliveryNote|";
    request = request + ")";

         var xmlWriter = XmlWriter.Create("\\"+"\\YourServer\\PDF_Merger\\trigger"+dlixBox.Text+".xml");













MEC mapping

Some .jar are required to read the IDM database and to merge the .pdf files. We already have icp.jar at our disposal.

A quick search on a famous search engine and we find the wonderful and free (!)  pdfbox-app-2.0.2.jar that will do the job for us.


In the ION mapper, import those .jar to the library :


The mapping is reduced the following one :

  • one function to retrieve the urls of all the .pdf files based on the user’s document selection + build the report in HTML
            // Create and connect the connection
            com.infor.daf.icp.Connection conn = new com.infor.daf.icp.Connection("https://yourServer:20108/ca/", "login", "password", com.infor.daf.icp.Connection.AuthenticationMode.BASIC);
            String FlagFirst = "1";
            String docType = "";
            org.apache.pdfbox.multipdf.PDFMergerUtility ut = new org.apache.pdfbox.multipdf.PDFMergerUtility();
            // Execute an XQuery search and print the display name for all items 
            com.infor.daf.icp.CMItems items = com.infor.daf.icp.CMItems.search(conn, DocumentVariant, 0, 100);
            for(com.infor.daf.icp.CMItem item : items) 
                for(com.infor.daf.icp.CMResource res : item.getResources().values()) 
                            String InitHTML = "<h1>PDF Merger report</h1><TABLE COLS=\"3\" FRAME=\"ALL\" BORDER-COLOR=\"Black\" BORDER=\"1\" VALIGN TD=\"MIDDLE\">";
                            String Titles = "<tr><th BGCOLOR=\"Navy\"><font COLOR=\"WHITE\">Doc type</font></th><th BGCOLOR=\"Navy\" <font COLOR=\"WHITE\">Doc name</font></th><th BGCOLOR=\"Navy\" <font COLOR=\"WHITE\">Version</font></th></tr>";
                            TEXT = InitHTML + Titles;
                            FlagFirst = "0";
                        if(res.getFilename().contains("MMS480PF")) docType = "Packing list";
                        if(res.getFilename().contains("MWS610PF")) docType = "CMR";
                        if(res.getFilename().contains("OIS199PF")) docType = "Cust. invoice";
                        TEXT = TEXT + "<tr><td ALIGN =\"MIDDLE\">"+docType+"</td><td ALIGN =\"MIDDLE\">" +res.getFilename()+ "</td><td ALIGN =\"MIDDLE\">"+item.getVersion()+"</td></tr>";
                System.out.println("\n NodeName : " + item.getNodeName());
                TEXT = TEXT + "</TABLE>";
            fileAttachment = getManifestInfo("agr:pathFrom")+"Docs_"+iValue+".pdf";
        catch(Exception e) 
        EMAIL = DocumentNumber;
        DLIX = iValue;
  • one function to send an email with attachment
            // SMTP
            java.util.Properties props = new java.util.Properties();
            props.put("mail.smtp.host", constMailServer);

            // session
            javax.mail.Session session = javax.mail.Session.getDefaultInstance(props, null);

            javax.mail.internet.InternetAddress addressFrom = new javax.mail.internet.InternetAddress(constFrom);

            // Create msg
            javax.mail.Message msg = new javax.mail.internet.MimeMessage(session);
            javax.mail.internet.InternetAddress addressTo[] = javax.mail.internet.InternetAddress.parse(EMAIL);
            msg.setRecipients(javax.mail.Message.RecipientType.TO, addressTo);
            msg.setSubject("PDF MERGED documents for delivery "+DLIX);
            javax.mail.internet.MimeBodyPart messageBodyPart = new javax.mail.internet.MimeBodyPart();
            javax.mail.Multipart multipart = new javax.mail.internet.MimeMultipart();
            messageBodyPart = new javax.mail.internet.MimeBodyPart();
            javax.activation.DataSource source = new javax.activation.FileDataSource(fileAttachment);
            messageBodyPart.setDataHandler(new javax.activation.DataHandler(source));
        catch(javax.mail.MessagingException e)
            throw new MeCError(e.getMessage());



Last but not the least, the final result in a screenshot :


The attachment’s name contains the delivery number and the report displays all the .pdf retrieved from IDM.

Note : there are 3 invoices in the delivery (according to customer settings), 1 packing list and 1 CMR (with a duplicate…)


That’s it! Happy coding !




Get an image from Infor Document Archive with Java

For my project to get M3 picking lists in Google Glass, I had to learn how to retrieve in Java an image that is stored in Infor Document Archive. The technique is probably well explained and documented somewhere, but I don’t work with Document Archive so I didn’t know where to start. So as a starting point, I used one of the Java examples built-in Document Archive 10.1.x, and from there I adapted the code for the older Document Archive 10.0.x. I share my results here with you in case you too need to retrieve resources in Java from Document Archive and don’t know where to start. I will later use that code in my Infor Grid application to insert the picture of the item in the picking list in Glass.

Built-in examples

Document Archive 10.1.x is built-in with some examples in Java to connect to the server and retrieve resources. I compiled that code and it ran out of the box. I then used it as a basis to adapt the code to the older Document Archive 10.0.x for which I couldn’t find built-in examples. I will show you the code for both versions. I will also show how to encode the bytes to Base64 as I might need it later for the Google Glass Mirror API.


Code for Document Archive 10.1.x

Here is the Java source code for Document Archive 10.1.x:

import java.io.FileOutputStream;
import java.io.InputStream;
import com.infor.daf.icp.CMItem;
import com.infor.daf.icp.CMResource;
import com.infor.daf.icp.Connection;
import com.infor.daf.icp.SearchQueries;
import com.infor.daf.icp.SearchQuery;
import com.infor.daf.icp.Connection.AuthenticationMode;
import org.apache.commons.codec.binary.Base64;

public class Test {
	public static void main(String[] args) {
		try {
			String baseUrl = "https://hostname:26108/ca/";
			String username = "JOHNDOE";
			String password = "*******";
			String query = "/M3_ITEM_IMAGE[@M3_ITNO = \"ACME\"]";
			Connection conn = new Connection(baseUrl, username, password, AuthenticationMode.BASIC);
			CMItem item = CMItem.search(conn, new SearchQueries(new SearchQuery(query)));
			for(CMResource res : item.getResources().values()) {
				// to file
				FileOutputStream fos = new FileOutputStream(res.getFilename());
				InputStream is = res.getUrlStream();
				CMResource.streamData(is, fos, true);
				// to URL
				// to Base64
				byte[] bytes = CMResource.createByteArray(res.getUrlStream());
				System.out.println(new String(Base64.encodeBase64(bytes)));
		} catch(Exception e) {

Find the Java library for Document Archive 10.1.x, icp.jar, and the required open source libraries httpclient, httpcore, commons-logging, commons-codec, and jaxen:



Compile and run the code with:

javac -extdirs . Test.java
java -cp icp.jar;httpclient-4.3.2.jar;httpcore-4.3.1.jar;commons-logging-1.1.1.jar;commons-codec-1.6.jar;jaxen.jar;. Test

The application will connect to Document Archive, will search the image by query, will retrieve the resource and will save it to a file, for example to JPEG.

The application will also show the URL to the resource, for example:


The application will also show the Base64-encoded bytes of the resource.

Code for Document Archive 10.0.x

Here is the Java source code for the older Document Archive 10.0.x. It is very similar to the newer code above, the package names are different (intentia instead of infor) and some methods are older. I had to decompile icp.jar to learn and adapt the code.

import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Iterator;
import org.apache.commons.codec.binary.Base64;
import com.intentia.icp.common.CMItem;
import com.intentia.icp.common.CMResource;
import com.intentia.icp.common.Connection;

public class Test {
	public static void main(String[] args) {
		try {
			String baseUrl = "https://hostname:25194/ca/";
			String username = "JOHNDOE";
			String password = "*******";
			String query = "/ESA_ItemImage[@ESA_ItemNumber = \"ACME\"]";
			Connection conn = new Connection(baseUrl, username, password);
			CMItem item = CMItem.search(conn, query);
			Iterator it = item.getResources().iterator();
			while (it.hasNext()) {
				CMResource res = (CMResource)it.next();
				// to XML
				// to file
				if (res.getEntityName().equals("ICMBASE")) {
					InputStream is = res.retrieveResource(conn);
					FileOutputStream fos = new FileOutputStream(res.getOrgFileName());
					CMResource.streamData(is, fos);
					// to Base64
					byte[] bytes = CMResource.createByteArray(res.retrieveResource(conn));
					System.out.println(new String(Base64.encodeBase64(bytes)));
		} catch(Exception e) {

Find the Java library for Document Archive 10.0.x, icp.jar, and the required open source libraries httpclient, httpcore, commons-logging, commons-codec, jaxen, and commons-io:



Compile and run the code with:

javac -extdirs . Test.java
java -cp icp.jar;jaxen-1.1.1.jar;httpclient-4.2.2.jar;httpcore-4.2.2.jar;commons-io-2.4.jar;commons-logging-1.1.1.jar;commons-codec-1.7.jar;. Test

The application will connect to Document Archive, will search the image by query, will retrieve the resource and will save it to a file, for example to JPEG.

The application will also show the XML of the resource, for example:

<?xml version="1.0" encoding="UTF-16" standalone="no"?>
    <pid>85 3 ICM8 icmnlsdb7 ICMBASE58 26 A1001001A14F20A45344I4341718 A14F20A45344I434171 13 300</pid>

The application will also show the Base64-encoded bytes of the resource.


That’s it. If you liked this, please click Like, leave me your thoughts in the comments below, and share your solutions by writing posts.

Also, please join the campaign and sign the petition to Infor so they make more of their source code available such that we can all learn more and make better solutions.


Thank you.


How to get an item image from Document Archive

Today I will illustrate how to get an item image from Infor Document Archive.

For that, I will use Document Archive Client to add an item image with the item number (ITNO) as an attribute, and I will retrieve the item image using the Document Archive REST API. I will do the illustration with two servers on different version numbers, one server running Document Archive version and the other server running version

I will later use these steps for my Google Glass project to display in Glass the item image of each picking line.

Document Archive Client in Smart Office

If you have Document Archive installed in your Grid, you will find the Document Archive Client in the Smart Office Navigator widget:

Document Archive web interface

Document Archive has a web interface at /ca/index.html.

Document Archive 10.0.x will simply return the version number:

Document Archive 10.1.x has a much richer web interface with a web client, an admin client, a mobile client, a Ming.le part, management pages, and an API overview:

The path /ca/impl/connection/information will return the API version number:

How to add an item image

To add an item image to Document Archive:

  1. Select Add Document > Item Image
  2. Drop an image file
  3. Set Status to Approved
  4. Enter an Item Number (ITNO)
  5. Click Save


How to search for an item image

To search for an Item Image by item number (ITNO):

  1. Select Attribute Search
  2. Set Document Type to Item Image
  3. Set Attribute to Item Number
  4. Set Operator to =
  5. Enter the item number in Value
  6. Click Search


Document Archive Client will return a list of possible matches, and for each match it will return the image converted into different sizes like original, preview, and thumbnail.

HTTP Requests and query

When I intercept those steps in Fiddler, I see four HTTP Requests, starting with the query:

The server with Document Archive 10.0.x used the query /ESA_ItemImage[@ESA_ItemNumber = “ACME”], and the other server with Document Archive 10.1.x used the query /M3_ITEM_IMAGE[@M3_ITNO = “ACME”]. I’m not fully familiar with how Document Archive is configured, so I don’t know if the query is based on the version number or if somebody did a manual configuration. So check what the query is on your server.

From the query, the server returns a list of matches, and for each match it gives the PID of the document and a list of resources with URLs to the item image in various sizes: original, preview, and thumbnail.

I’m only interested in the original item image, that’s entityName=ICMBASE. The PID would be given by the following XPath on the API response:


Document Archive REST API

Document Archive has a REST API that we can access from http://host:port/ca .

The path /ca/application.wadl will return the Web Application Description Language (WADL) of the API:

Also, Document Archive 10.1.x has a richer API, and a really well polished award-winning documentation and playground:

Search an item image with the REST API

There are several API available to get the item image.

To determine which API accepts query as an input parameter, use the following XPath on the application.wadl:

//param[contains(@name, 'query')]

To determine which API returns binary data (image bytes) as output, use the following XPath:

//representation[contains(@mediaType, 'application/octet-stream')]

By trial and error, I determined I could get the item image in only one request with the following API, the parameter $query properly URL-encoded, and HTTP Basic Authentication:




It wasn’t easy. I ran into a lot of errors which I still don’t understand and which I didn’t fully document. Here are a few bits and pieces I noted:

  • Unfortunately, the WADL doesn’t mention the input parameter QK_xquery for the searchItems.jsp API, therefore there may be more undocumented API that also accept QK_xquery as an input parameter; to be tested.
  • In some of my tests the server threw HTTP 401 Unauthorized which led me to believe the user/password was wrong. It turns out the Grid session provider didn’t allow HTTP and only allowed HTTPS. Very misleading error message.
  • I got a lot of cryptic prolog errors: “Parsing error: Invalid input data: Content is not allowed in prolog. org.xml.sax.SAXParseException”. It seems to be a bug in Document Archive for POST methods and the workaround is to manually change the HTTP Request header to Content-Type: text/plain.
  • I got a lot of inexplicable java.lang.NullPointerException even with the correct input parameters.
  • I got a lot of inexplicable HTTP/1.1 500 Internal Server Error even with the correct input parameters.


That was quick overview of how to add an item image by item number (ITNO) in Document Archive, and how to retrieve it with the REST API.

That’s it! Please comment, like, share, follow, enjoy, author. Thank you.