Writing classes to encapsulate a record layout in a JMSBytesMessage
The purpose of this task is to explore, by example, how to combine data conversion and a fixed record layout in a JMSBytesMessage. In the task, you create some Java classes to exchange an example record structure in a JMSBytesMessage. We can modify the example to write classes to exchange other record structures.
A JMSBytesMessage is the best choice of JMS message type to exchange mixed data type records with non-JMS programs. It has no additional data inserted into the message body by the JMS provider. It is therefore the best choice of message type to use if a JMS client program interoperates with an existing IBM MQ program. The main challenge in using a JMSBytesMessage comes with matching the encoding and character set expected by the other program. A solution is to create a class that encapsulates the record. A class that encapsulates reading and writing a JMSBytesMessage, for a specific record type, makes it easier to send and receive fixed-format records in a JMS program. By capturing the generic aspects of the interface in an abstract class, much of the solution can be reused for different record formats. Different record formats can be implemented in classes that extend the abstract generic class.
An alternative approach is to extend the com.ibm.mq.headers.Header class. The Header class has methods, such as addMQLONG, to build a record format in a more declarative way. A disadvantage of using the Header class is getting and setting attributes uses a more complicated interpretative interface. Both approaches result in much the same amount of application code.
A JMSBytesMessage can encapsulate only a single format, in addition to an MQRFH2, in one message, unless each record uses the same format, coded character set, and encoding. The format, encoding, and character set of a JMSBytesMessage are properties of all of the message following the MQRFH2. The example is written on the assumption that a JMSBytesMessage contains only one user record.
Before starting
- Your skill level: we must be familiar with Java programming and JMS. No instructions are provided about setting up the Java development environment. It is advantageous to have written a program to exchange a JMSTextMessage, JMSStreamMessage, or JMSMapMessage. We can then see the differences in exchanging a message using a JMSBytesMessage.
- The example requires IBM WebSphere MQ Version 7.0.
- The example was created using the Java perspective of the Eclipse workbench. It requires JRE 6.0 or higher. We can use the Java perspective in IBM MQ Explorer to develop and run the Java classes. Alternatively, use your own Java development environment.
- Use IBM MQ Explorer makes setting up the test environment, and debugging, simpler than using command-line utilities.
You are guided through creating two classes: RECORD and MyRecord. Together these two classes encapsulate a fixed-format record. They have methods to get and set attributes. The get method reads the record from a JMSBytesMessage and the put method writes a record to a JMSBytesMessage.
The purpose of the task is not to create a production quality class that we can reuse. We might choose to use the examples in the task to get started on your own classes. The purpose of the task is to provide you with guidance notes, primarily about using character sets, formats, and encoding, when using a JMSBytesMessage. Each step in creating the classes is explained, and aspects of using JMSBytesMessage, which are sometimes overlooked, are described.
The RECORD class is abstract and defines some common fields for a user record. The common fields are modeled on the standard IBM MQ header layout of having an eye catcher, a version, and a length field. The encoding, character set, and format fields, found in many IBM MQ headers, are omitted. Another header cannot follow a user-defined format. The MyRecord class, which extends the RECORD class, does so by literally extending the record with additional user fields. A JMSBytesMessage, created by the classes, can be processed by the queue manager data conversion exit.
Classes used to run example includes a full listing of RECORD and MyRecord. It also includes listings of the extrascaffoldingclasses to test the RECORD and MyRecord. The extra classes are:
- TryMyRecord
- The main program to test RECORD and MyRecord.
- EndPoint
- An abstract class that encapsulates the JMS connection, destination, and session in a single class. Its interface just meets the needs of testing the RECORD and MyRecord classes. It is not an established design pattern for writing JMS applications. Note: The Endpoint class includes this line of code after creating a destination:
((MQDestination)destination).setReceiveConversion (WMQConstants.WMQ_RECEIVE_CONVERSION_QMGR);In V7.0, from V7.0.1.5, it is necessary to turn on queue manager conversion. It is disabled by default. In V7.0, up to V7.0.1.4 queue manager conversion is enabled by default, and this line of code causes an error.
- MyProducer and MyConsumer
- Classes that extend EndPoint, and create a MessageConsumer and MessageProducer, connected and ready to accept requests.
Together all the classes make up a complete application we can build and experiment with, to understand how to use data conversion in a JMSBytesMessage.
Procedure
- Create an abstract class to encapsulate the standard fields in an IBM MQ header, with a default constructor. Later, you extend the class to tailor the header to we requirements.
public abstract class RECORD implements Serializable { private static final long serialVersionUID = -1616617232750561712L; protected final static int UTF8 = 1208; protected final static int MQLONG_LENGTH = 4; protected final static int RECORD_STRUCT_ID_LENGTH = 4; protected final static int RECORD_VERSION_1 = 1; protected final String RECORD_STRUCT_ID = "BLNK"; protected final String RECORD_TYPE = "BLANK "; private String structID = RECORD_STRUCT_ID; private int version = RECORD_VERSION_1; private int structLength = RECORD_STRUCT_ID_LENGTH + MQLONG_LENGTH * 2; private int headerEncoding = WMQConstants.WMQ_ENCODING_NATIVE; private String headerCharset = "UTF-8"; private String headerFormat = RECORD_TYPE; public RECORD() { super(); }Note:
- The attributes, structID to nextFormat, are listed in the order they are laid out in a standard IBM MQ message header.
- The attributes, format, messageEncoding, and messageCharset, describe the header itself, and are not part of the header.
- We must decide whether to store the coded character set identifier or character set of the record. Java uses character sets and IBM MQ messages use coded character set identifiers. The example code uses character sets.
- int is serialized to MQLONG by IBM MQ. MQLONG is 4 bytes.
- Create the getters and setters for the private attributes.
- Create or generate the getters:
public String getHeaderFormat() { return headerFormat; } public int getHeaderEncoding() { return headerEncoding; } public String getMessageCharset() { return headerCharset; } public int getMessageEncoding() { return headerEncoding; } public String getStructID() { return structID; } public int getStructLength() { return structLength; } public int getVersion() { return version; }- Create or generate the setters:
public void setHeaderCharset(String charset) { this.headerCharset = charset; } public void setHeaderEncoding(int encoding) { this.headerEncoding = encoding; } public void setHeaderFormat(String headerFormat) { this.headerFormat = headerFormat; } public void setStructID(String structID) { this.structID = structID; } public void setStructLength(int structLength) { this.structLength = structLength; } public void setVersion(int version) { this.version = version; } }
- Create a constructor to create a RECORD instance from a JMSBytesMessage.
public RECORD(BytesMessage message) throws JMSException, IOException, MQDataException { super(); setHeaderCharset(message.getStringProperty(WMQConstants.JMS_IBM_CHARACTER_SET)); setHeaderEncoding(message.getIntProperty(WMQConstants.JMS_IBM_ENCODING)); byte[] structID = new byte[RECORD_STRUCT_ID_LENGTH]; message.readBytes(structID, RECORD_STRUCT_ID_LENGTH); setStructID(new String(structID, getMessageCharset())); setVersion(message.readInt()); setStructLength(message.readInt()); }Note:
- The messageCharset and messageEncoding, are captured from the message properties, as they override the values set for the destination. format is not updated. The example does no error checking. If the Record(BytesMessage) constructor is called, it is assumed that the JMSBytesMessage is a RECORD type message. The line
setStructID(new String(structID, getMessageCharset()))sets the eye catcher.- The lines of code that complete the method deserialize fields in the message, in order, updating the default values set in the RECORD instance.
- Create a put method to write the header fields to a JMSBytesMessage.
protected BytesMessage put(MyProducer myProducer) throws IOException, JMSException, UnsupportedEncodingException { setHeaderEncoding(myProducer.getEncoding()); setHeaderCharset(myProducer.getCharset()); myProducer.setMQClient(true); BytesMessage bytes = myProducer.session.createBytesMessage(); bytes.setStringProperty(WMQConstants.JMS_IBM_FORMAT, getHeaderFormat()); bytes.setIntProperty(WMQConstants.JMS_IBM_ENCODING, getHeaderEncoding()); bytes.setIntProperty(WMQConstants.JMS_IBM_CHARACTER_SET, myProducer.getCCSID()); bytes.writeBytes(String.format("%1$-" + RECORD_STRUCT_ID_LENGTH + "." + RECORD_STRUCT_ID_LENGTH + "s", getStructID()) .getBytes(getMessageCharset()), 0, RECORD_STRUCT_ID_LENGTH); bytes.writeInt(getVersion()); bytes.writeInt(getStructLength()); return bytes; }Note:
- MyProducer encapsulates the JMS Connection, Destination, Session, and MessageProducer in a single class. MyConsumer, used later on, encapsulates the JMS Connection, Destination, Session, and MessageConsumer in a single class.
- For a JMSBytesMessage, if the encoding is other than Native, the encoding must be set in the message. The destination encoding is copied to the message encoding attribute, JMS_IBM_CHARACTER_SET, and saved as an attribute of the RECORD class.
setMessageEncoding(myProducer.getEncoding());calls(((MQDestination) destination).getIntProperty(WMQConstants.WMQ_ENCODING));to get the destination encoding. Bytes.setIntProperty(WMQConstants.JMS_IBM_ENCODING, getMessageEncoding());sets the message encoding.
The character set used to transform text into bytes is obtained from the destination, and saved as an attribute of the RECORD class. It is not set in the message, because it is not used by the IBM MQ classes for JMS when writing a JMSBytesMessage.
messageCharset = myProducer.getCharset();callspublic String getCharset() throws UnsupportedEncodingException, JMSException { return CCSID.getCodepage(getCCSID()); }It gets the Java character set from a coded character set identifier.
CCSID.getCodepage(ccsid)is in the package com.ibm.mq.headers. The ccsid is obtained from another method in MyProducer, which queries the destination:public int getCCSID() throws JMSException { return (((MQDestination) destination) .getIntProperty(WMQConstants.WMQ_CCSID)); }
myProducer.setMQClient(true);overrides the destination setting for the client type, forcing it to an IBM MQ MQI client. We might prefer to omit this line of code, as it obscures an administrative configuration error.myProducer.setMQClient(true);calls:((MQDestination) destination).setTargetClient(WMQConstants.WMQ_TARGET_DEST_MQ); } if (!getMQDest()) setMQBody();The code has the side-effect of setting the IBM MQ body style to unspecified, if it must override a setting of JMS. Note:The IBM MQ classes for JMS write the format, encoding, and character set identifier of the message into the message descriptor, MQMD, or into the JMS header, MQRFH2. It depends on whether the message has an IBM MQ style body. Do not set the MQMD fields manually.
A method exists to set the message descriptor properties manually. It uses the JMS_IBM_MQMD_* properties. We must set the destination property, WMQ_MQMD_WRITE_ENABLED to set the JMS_IBM_MQMD_* properties:((MQDestination)destination).setMQMDWriteEnabled(true);You must set the destination property, WMQ_MQMD_READ_ENABLED, to read the properties.Use the JMS_IBM_MQMD_* only if you take full control over the whole message payload. Unlike the JMS_IBM_* properties, the JMS_IBM_MQMD_* properties do not control how IBM MQ classes for JMS construct a JMS message. It is possible to create message descriptor properties that conflict with the properties of the JMS message.
- The lines of code that completes the method serialize the attributes in class as fields in the message.
- The string attributes are padded with blanks. The strings are converted to bytes using the character set defined for the record, and truncated to the length of the message fields.
- Complete the class by adding the imports.
package com.ibm.mq.id; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import javax.jms.BytesMessage; import javax.jms.JMSException; import com.ibm.mq.constants.MQConstants; import com.ibm.mq.headers.MQDataException; import com.ibm.msg.client.wmq.WMQConstants;- Create a class to extend the RECORD class to include additional fields. Include a default constructor.
public class MyRecord extends RECORD { private static final long serialVersionUID = -370551723162299429L; private final static int FLAGS = 1; private final static String STRUCT_ID = "MYRD"; private final static int DATA_LENGTH = 32; private final static String FORMAT = "MYRECORD"; private int flags = FLAGS; private String recordData = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"; public MyRecord() { super(); super.setStructID(STRUCT_ID); super.setHeaderFormat(FORMAT); super.setStructLength(super.getStructLength() + MQLONG_LENGTH + DATA_LENGTH); }Note:
- The RECORD subclass, MyRecord, customizes the eye catcher, format, and length of the header.
- Create or generate the getters and setters.
- Create the getters:
public int getFlags() { return flags; } public String getRecordData() { return recordData; } .- Create the setters:
public void setFlags(int flags) { this.flags = flags; } public void setRecordData(String recordData) { this.recordData = recordData; } }
- Create a constructor to create a MyRecord instance from a JMSBytesMessage.
public MyRecord(BytesMessage message) throws JMSException, IOException, MQDataException { super(message); setFlags(message.readInt()); byte[] recordData = new byte[DATA_LENGTH]; message.readBytes(recordData, DATA_LENGTH); setRecordData(new String(recordData, super.getMessageCharset())); }Note:
- The fields that make up the standard message template are read first by the RECORD class.
- The recordData text is converted to String using the character set property of the message.
- Create a static method to get a message from a consumer and create a new MyRecord instance.
public static MyRecord get(MyConsumer myConsumer) throws JMSException, MQDataException, IOException { BytesMessage message = (BytesMessage) myConsumer.receive(); return new MyRecord(message); }Note:
- In the example, for brevity, the MyRecord(BytesMessage) constructor is called from the static get method. Typically, you might separate receiving the message from creating a new MyRecord instance.
- Create a put method to append the customer fields to a JMSBytesMessage containing a message header.
public BytesMessage put(MyProducer myProducer) throws JMSException, IOException { BytesMessage bytes = super.put(myProducer); bytes.writeInt(getFlags()); bytes.writeBytes(String.format("%1$-" + DATA_LENGTH + "." + DATA_LENGTH + "s",getRecordData()) .getBytes(super.getMessageCharset()), 0, DATA_LENGTH); myProducer.send(bytes); return bytes; }Note:
- The method calls in the code serialize the attributes in the MyRecord class as fields in the message.
- The recordData String attribute is padded with blanks, converted to bytes using the character set defined for the record, and truncated to the length of the RecordData fields.
- Complete the class by adding the include statements.
package com.ibm.mq.id; import java.io.IOException; import javax.jms.BytesMessage; import javax.jms.JMSException; import com.ibm.mq.headers.MQDataException;
Results
Results:
- The results from running the TryMyRecord class:
- Send message in coded character set 37, and using a queue manager conversion exit:
Out flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 546 CCSID 37 MQ true Out flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 546 CCSID 37 MQ true In flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 273 CCSID UTF-8- Send message in coded character set 37, and not using a queue manager conversion exit:
Out flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 546 CCSID 37 MQ true Out flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 546 CCSID 37 MQ true In flags 1 text ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 Encoding 546 CCSID IBM037
- The results from modifying the TryMyRecord class not to receive the message, and instead receiving it using the modified amqsget0.c sample. The modified sample accepts a formatted record; see Figure 2 in Exchanging a formatted record with a non-JMS application.
- Send message in coded character set 37, and using a queue manager conversion exit:
Sample AMQSGET0 start ccsid <850>, flags <1>, message <ABCDEFGHIJKLMNOPQRSTUVWXYZ012345> no more messages Sample AMQSGET0 end- Send message in coded character set 37, and not using a queue manager conversion exit:
Sample AMQSGET0 start MQGET ended with reason code 2110 ccsid <37>, flags <1>, message <--+-+ãÃ++ÐÊËÈiÐÎÐ+ÔÒõõμþÞÚ-±=¾¶§> no more messages Sample AMQSGET0 end
To try out the example and experiment with different code pages and a data conversion exit. Create the Java classes, configure IBM MQ, and run the main program, TryMyRecord ; see Figure 1.
- Configure IBM MQ and JMS to run the example. The instructions are for running the example on Windows.
- Create a queue manager
crtmqm -sa -u SYSTEM.DEAD.LETTER.QUEUE QM1 strmqm QM1- Create a queue
echo DEFINE QL('Q1') REPLACE | runmqsc QM1- Create a JNDI directory
cd c:\ md JNDI-Directory- Switch to the JMS bin directory
The JMS Administration program must be run from here. The path is MQ_INSTALLATION_PATH\java\bin.
- Create the following JMS definitions in a file called JMSQM1Q1.txt
DEF CF(QM1) PROVIDERVERSION(7) QMANAGER(QM1) DEF Q(Q1) CCSID(37) ENCODING(RRR) MSGBODY(MQ) QMANAGER(QM1) QUEUE(Q1) TARGCLIENT(MQ) VERSION(7) END- Run the JMSAdmin program to create the JMS resources
JMSAdmin < JMSQM1Q1.txt
- We can create, alter, and browse the definitions you have created using IBM MQ Explorer.
- Run TryMyRecord.
Classes used to run example
The classes listed in figures Figure 1 to Figure 6 are also available in a ZIP file; download jm25529_.zip or jm25529_.tar.gz. Parent topic: Exchanging a formatted record with a non-JMS application