The Wireless Messaging API 2.0

来源:百度文库 编辑:神马文学网 时间:2024/06/05 09:00:12
The Wireless Messaging API (WMA) provides a common interface you can use to enable an application based on the Mobile Information Device Profile (MIDP) to send and receive short text and binary messages, as well as multimedia messages. These messages typically are part of store-and-forward messaging systems such as the Short Messaging Service (SMS) and the Multimedia Messaging Service (MMS) that guarantee delivery of messages.
Originally introduced in theJava Community Process asJSR 120, WMA 1.1 has been enhanced and released as WMA 2.0 inJSR 205. You can find more information on the WMA, and a reference implementation, at theWMA technology page. This article covers the differences between versions 1.1 and 2.0, and shows you how to take full advantage of this messaging API.
Contents
- WMA Overview
- WMA 2.0 Message Types
- WMA Connections
- Creating a Message Connection
- Closing a Message Connection
- Creating and Sending Messages
- Creating and Sending a Text Message
- Creating and Sending a Binary Message
- Creating and Sending a Multipart Message
- Creating Message Parts
- Waiting for Incoming Messages
- Processing Messages
- Message Persistence
- About Segmentation and Reassembly
- System Properties
- WMA 2.0 and the Push Registry
- Security
- The WMA v2.0 Reference Implementation
- About Server-to-Handset Messaging
- Summary
- References
- Acknowledgments
- About the Author
 
WMA Overview

WMA targets cell phones and other devices that can send and receive wireless messages. It's a generic messaging API for sending not only individual text and binary messages but the multipart payloads typically used for transmitting multimedia messages.
How messages are delivered depends on the underlying transport, or bearer, such as GSM SMS, GSM CBS, CMDA SMS, or MMS. The message format and transport are defined by the respective standards, but for the most part WMA renders such details transparent to the application. It's important to note, though, that SMS and MMS transports are actually managed differently in the network, and that MMS is not just a means to transmit larger SMS packets. WMA doesn't place limits on message size and other restrictions — but do be aware that the underlying transports do, as you'll see shortly.
WMA is based on the Generic Connection Framework (GCF). It's defined as a J2ME optional package; that is, it contains specialized APIs that can be added to a software stack based on a standard configuration. WMA's lowest common denominator is the Connected Limited Device Configuration (CLDC). Because the Connected Device Configuration (CDC) is a superset of CLDC, WMA can be included in both CLDC- and CDC-based stacks. Figure 1 summarizes the components of the WMA 2.0:
Figure 1: of the Wireless Messaging API 2.0 (Click to enlarge.)
WMA, a required optional package
JTWI defines minimum and common characteristics that all compliant handsets must provide; it mandates support for WMA 1.1, but not WMA 2.0. MSA continues the evolution of mobile architectures and specifies WMA 2.0 to be a mandatory package.
Even though WMA is an optional package with regard to the Java Platform, Mobile Edition (Java ME) as a whole, its presence is mandatory on handsets that claim to comply withJSR 185, the Java Technology for the Wireless Industry (JTWI) specification, and with JSR 248, the new Mobile Service Architecture (MSA) for CLDC.
All WMA-specific interfaces and classes are contained in a single package, javax.wireless.messaging, which defines all the APIs required for sending and receiving wireless text, binary, and multi-part messages. Table 1 summarizes this package:
Interface
Description
Methods
1.1
2.0

Message
Base message interface, from which subinterfaces such as BinaryMessage, TextMessage, and MultipartMessage are derived
getAddress()
getTimestamp()
setAddress()
X
X
BinaryMessage
Subinterface of Message that represents a binary message and provides methods to set and get the binary payload
getPayloadData()
setPayloadData()
X
X
TextMessage
Subinterface of Message that represents a text message and provides methods to set and get the text payload
getPayloadText()
setPayloadText()
X
X
MessageConnection
Subinterface of the GCF Connection, which provides a factory of Messages, and methods to send and receive Messages
newMessage()
receive()
send()
setMessageListener()
numberOfSegments()
X
X
MessageListener
Defines the listener interface to implement asynchronous notification of Message objects
notifyIncomingMessage()
X
X
MessagePart
Defines a message part that can be added to a MultipartMessage
getContent()
getContentAsStream()
getContentID()
getContentLocation()
getEncoding()
getLength()
getMIMEType()
X
MultipartMessage
Subinterface of Message that represents a multi-part message and provides methods to set and get multiple payloads
addAddress()
addMessagePart()
getAddress()
getAddresses()
getHeader()
getMessagePart()
getMessageParts()
getStartContentId()
getSubject()
removeAddress()
removeAddresses()
removeMessagePart()
removeMessagePartId()
removeMessagePartLocation()
setAddress()
setHeader()
setStartContentId()
setSubject()
X
SizeExceededException
Exception thrown if the multipart message content is larger than the available memory or supported size for the message part
--
X

The differences between WMA versions 1.1 and 2.0 are related primarily to the support of multi-part messages used for MMS messaging. The next several sections provide some of the particulars of the different components of WMA. For the low-level details of each of the WMA methods, consult the WMA specification.
WMA 2.0 Message Types

WMA 2.0 defines four message representations, or types. In addition, it defines a message part in support of multipart messages, used for carrying multimedia messages. The next five sections describe the interfaces that represent messages, and the class that represents a message part.
The Message Interface
The interface javax.wireless.messaging.Message is the base type for all messages communicated using WMA - a Message is what is sent and received, produced and consumed. In some respects, a Message looks similar to a datagram: it has source and destination addresses, and a payload.
There are significant differences too. A WMA Message includes a timestamp, and supports multiple payload types, as defined by its subinterfaces. Moreover, because wireless messaging is typically implemented on top of store-and-forward systems such as SMS and MMS, message delivery is reliable, guaranteed, and even traceable.
The interface specifies methods to get and set the message's source and destination addresses, and to get its timestamp:
String getAddress(); void setAddress(String address); Date getTimestamp();
WMA 2.0 defines three subinterfaces of Message, seen in Figure 2:
Figure 2: The Message Interface and its Subinterfaces (Click to enlarge.)
Where:
BinaryMessage is for short binary messages typically over SMS. TextMessage is for short text messages typically over SMS. MultipartMessage is for multi-media messages typically over MMS.
The BinaryMessage Interface
The BinaryMessage subinterface represents a message with a binary payload, typically an SMS-based short binary message. A binary payload is encoded using 8-bit data, allowing 140 bytes per segment — 133 if a port number is used. For more information about message segmentation refer to the section "About Segmentation and Reassembly" later in this article. This interface declares methods to set and get the binary payload as an array of bytes:
byte[] getPayloadData(); void setPayloadData(byte[] bytes);
Methods to set and get the address of the message, and to get its timestamp, are all inherited from Message.
The TextMessage Interface
The TextMessage subinterface represents a message with a text payload, typically an SMS-based short text message. This interface provides methods to set and get the text as an instance of String:
String getPayloadText(); void setPayloadText(String data);
The underlying implementation is responsible for properly encoding or decoding the String to or from the appropriate format before the text message is sent or received. The character encoding that's used affects the size of the message. For example, GSM 7-bit allows 160 characters per segment, or 152 if a port number is used, while UCS-2 allows for 70 double-byte characters per segment, or 66 if a port number is used. Again, refer to the section "About Segmentation and Reassembly" for more information. As with BinaryMessage, the methods to set and get the address of the message and get its timestamp are inherited from Message.
The MultipartMessage Interface
The MultipartMessage subinterface represents a message that consists of multiple parts, typically an MMS-based multimedia message. This interface defines a container of one or more MessageParts, and provides methods to manage the sender and recipient addresses, the message's headers, the "start message" content ID, and the message's parts:
boolean addAddress(String type, String address); void addMessagePart(MessagePart messagePart) throws SizeExceededException; String getAddress(); String[] getAddresses(String type); String getHeader(String headerField); MessagePart getMessagePart(String contentID); MessagePart[] getMessageParts(); String getStartContentId(); String getSubject(); boolean removeAddress(String type, String address); void removeAddresses(); void removeAddresses(String type); boolean removeMessagePart(MessagePart messagePart); boolean removeMessagePartId(String contentID); boolean removeMessagePartLocation(String contentLocation); void setAddress(String address); void setHeader(String headerField, String headerValue); void setStartContentId(String contentID); void setSubject(String subject);
MultipartMessage overrides the methods to set and get the message's address it inherits from Message.
Multipart messages follow the format of standard emails, which consist of RFC822-based headers and multiple parts based on the Multipurpose Internet Mail Extensions (MIME) standard defined by the World Wide Web Consortium (W3C) inRFC2045 andRFC2046, as Figure 3 illustrates:
Figure 3: Structure of an MMS Multipart Message (Click to enlarge.)
The MultipartMessage interface represents the multimedia message and its headers, while a MessagePart class instance represents each individual MIME part.
The MessagePart Class
As its name suggests, MessagePart represents one part of a message. In addition to various constructors, this class provides methods to retrieve the content, and information about the content:
MessagePart(byte[] contents, int offset, int length, String mimeType, String contentId, String contentLocation, String encoding) throws SizeExceededException; MessagePart(byte[] contents, String mimeType, String contentId, String contentLocation, String encoding) throws SizeExceededException; MessagePart(java.io.InputStream is, String mimeType, String contentId, String contentLocation, String encoding) throws IOException, SizeExceededException; public byte[] getContent(); public InputStream getContentAsStream(); public String getContentID(); public String getContentLocation(); public String getEncoding(); public int getLength(); public String getMIMEType();
A message part consists of a MIME type as defined in RFC2046, a content ID, a content location, and the content itself. You can construct a MessagePart from a byte array or a java.io.InputStream.
WMA Connections

In the Wireless Messaging API, input, output, and network connectivity are based on theGeneric Connection Framework (GCF). WMA connections are based on the MessageConnection interface, which is a subinterface of GCF's javax.microedition.io.Connection, as illustrated in the next figure:
Figure 4: The MessageConnection and Its Relationship to the GCF
The MessageConnection interface defines factory methods for creating TextMessages, BinaryMessages, and MultipartMessages, a method to calculate the number of protocol segments needed for sending the message, methods to receive and send messages, and a method to set the message listener for this connection:
Message newMessage(String type); Message newMessage(String type, String address); int numberOfSegments(Message message); Message receive() throws IOException, InterruptedIOException; void send(Message message) throws IOException, InterruptedIOException; void setMessageListener(MessageListener messageListener) throws IOException;
In addition, the interface defines String constants, one of which you pass to the newMessage() factory method to identify the type of Message to create:
String TEXT_MESSAGE = "text"; String BINARY_MESSAGE = "binary"; String MULTIPART_MESSAGE = "multipart";
When creating a message connection, always use one of these constants. Note that the WMA implementation uses String.equals() to compare the string values, making these message types case-sensitive.
You open and close a MessageConnection just as you do any other GCF Connection. To create the connection, call the connection factory's method javax.microedition.io.Connector.open(). To close it call the appropriate method of the base connection interface, javax.microedition.io.Connection.close(). You can have multiple MessageConnections open simultaneously.
You can create a MessageConnection in either of two modes: as a client connection or as a server connection. A client connection can only send messages, while a server connection can both send and receive. As with other GCF connections, you specify a connection as either client or server by way of the URL, as here:
scheme://address-part [params]
Where:
scheme is the protocol to use address-part is the protocol-specific address for client or server connections params are scheme-dependent optional parameters in the form ;x=y
WMA 2.0 Supported Protocol Adapters and Standards
Support for WMA protocol adapters is based on the following standards: Short Messaging System as defined in the Global System for Mobile Communications (GSM) 03.40 standard Cell Broadcast Service as defined in the Global System for Mobile Communications (GSM) 03.41 standard Short Messaging System as defined in the Code Division Multiple Access (CDMA) IS-637 standard Multimedia Messaging Services and the Protocol Data Unit (PDU) structure as defined in the WAP-209-MMS-Encapsulation standard
The URL scheme for creating a MessageConnection is not specific to a single protocol. Instead it is intended to support many wireless messaging protocols. The WMA specification defines the following messaging protocol adapters:
sms for Short Messaging System. SMS is bi-directional: you can create, send, and receive messages — and thus support both client and server connections.
mms for Multimedia Messaging System. Like SMS, MMS is bi-directional, and thus supports both client and server connections.
cbs for Cell Broadcast Short Message. CBS messages are broadcast by a base station, and can only be received; attempting to send over a cbs connection results in an IOException. CBS messages don't have a timestamp; trying to get a message's timestamp returns a null.
The address-part of the URL specifies the handset and application. It also specifies whether the connection is a client or a server. A URL for a client connection includes a full destination address, while the URL for a server connection specifies a local address — no host, just a protocol-specific local address, typically a port number or an application ID. For sms the address-part comprises an MSISDN that identifies the handset, a port number that identifies the application, or both. For mms, the address-part could consist of an email address, phone number, IPv4, IPv6, or shortcode-address, or an application ID, or both. For cbs, which supports only receiving messages (server mode), the address-part is only a port number that identifies the application.
Examples of client connection URLs:
(MessageConnection)Connector.open ("sms://+15121234567:5000"); (MessageConnection)Connector.open ("mms://+15121234567: com.j2medeveloper.MyMmsApp");
No example for cbs is included, as cbs supports only server connections.
Examples of server connection URLs:
(MessageConnection)Connector.open("sms://:5000"); (MessageConnection)Connector.open("mms://:com.j2medeveloper.MyMmsApp"); (MessageConnection)Connector.open("cbs://:6000");
About Application IDs and Port Numbers
Application IDs identify the receiving application on a handset. For example, in the two URLs sms://+5121234567:5000 and mms://+5121234567:com.j2medeveloper.MyMmsApp, the number 5000 and the string com.j2medeveloper.MyMmsApp are both application identifiers. In sms and cbs, applications are identified by port numbers, similarly to how port numbers are used for TCP and UDP connections. In mms, applications are identified by a string that typically is a fully qualified name such as com.j2medeveloper.MyMmsApp rather than a port number. Note that a port number takes space, typically 16 bits, away from the actual content of a short message. MMS application IDs, which can't exceed 32 characters in length, are optionally included as part of the multipart Content-Type message header. Creating an MMS server connection that specifies the application ID com.j2medeveloper.MyMmsApp will cause two parameters to be added to the value of the Content-Type message header:
Application-ID=com.j2medeveloper.MyMmsApp;
Reply-To-Application-ID= com.j2medeveloper.MyMmsApp
When a server connection is opened, a port number or application ID is specified (depending on the protocol used). A client wanting to send a message to that server application must indicate that same port number or application ID. The first application to bind to a given port number or application ID gets it, and attempting to bind to an already reserved local port number or application ID throws an IOException that your code must handle appropriately.
You can pick a random port number to use from the private/dynamic port range 49152-65635; the range 0-49151 is used by privileged applications or user applications such as WAP; refer to the WMA 2.0 specification for more information on which ones you should avoid. You can reserve your own port number for your application by contacting theInternet Assigned Numbers Authority (IANA).
Note that sending a message to a handset without specifying a port number or application ID to identify an application typically causes that message to be delivered to the handset's default viewer application.
For more information about URLs see the articles "The Generic Connection Framework," and "A Generic Connection Framework cheat sheet."
As Table 1 shows, MessageConnection provides the methods newMessage(), send(), and receive(), to create, send, and receive Message objects respectively. The numberOfSegments() method is of particular interest; it's used to determine segment information for a given Message before it is sent — see "About Segmentation and Reassembly" for more information. You use setMessageListener() to set the message listener that is called by the WMA implementation when new messages arrive.
WMA is a very intuitive, very simple-to-use API. Let's go over some code that shows how to take advantage of it.
Creating a Message Connection

To create a client MessageConnection you call the connection factory method Connector.open(), passing a URL that specifies a valid WMA messaging protocol, and that includes either the local address for a server connection, or a full destination address for a client connection:
... MessageConnection mc = null; String connUrl = "mms://:com.j2medeveloper.MyMmsApp"; // My MMS server app ... try { mc = (MessageConnection)Connector.open(connUrl); } catch (IOException ioe) { // Port number or application identifier is already reserved } catch (SecurityException se) { // Permission to open the MessageConnection was not granted } ...
It is good practice to define your application's port numbers and application identifiers in the Java Application Descriptor (JAD) file, and retrieve these at runtime, typically during application startup.
For instance, we can place the following attributes in the JAD file:
... SmsApplicationPort: 50000 CbsMessageID: 50001 MmsApplicationID: com.j2medeveloper.MyMmsApp ...
To retrieve each of these attributes, we call MIDlet.getAppProperty(String attributeName), passing it the name of the attribute to get. For illustration, let's define the helper method loadWmaAttributes() to retrieve the WMA port and application ID from the JAD:
... // Define the SMS and MMS Port and Application Identifiers String smsPort; String cbsMsgId; String mmsAppId; ... /** * Loads the application WMA attributes */ private void loadWmaAttributes() { mmsAppId = getAppProperty("MmsApplicationID"); smsAppId = getAppProperty("SmsApplicationPort"); cbsAppId = getAppProperty("CbsMessageID"); } ...
To illustrate how to create a connection, let's define the helper method newMessageConnection(), which creates and returns a MessageConnection:
/** * Create a new MessageConnection * * @param connUrl is the server or client URL for the connection * @param messageListener is the message listener for this * connection * @return a MessageConnection object * @throws a ConnectionNotFoundException if * the Connection target is not found, * or the protocol not supported * @throws an IOException if * the Port number or application identifier is already reserved, * or the connection has been closed, * or if an attempt is made to register a listener on a client * connection * @throws an IllegalArgumentException if * a Message type parameter is invalid * or attempting to create a *Stream * @throws SecurityException if * permission to open the MessageConnection was not granted, * or, when setting the message listener, it was determined * that the application does not have permission to receive * on the given port number */ final public MessageConnection newMessageConnection( String connUrl, MessageListener messageListener) throws ConnectionNotFoundException, IOException, IllegalArgumentException, SecurityException { MessageConnection mc = null; mc = (MessageConnection)Connector.open(connUrl); mc.setMessageListener(messageListener); return(mc); }
The connection factory returns a Connection subtype that must be properly cast; in our example, we cast the value returned by Connector.open() to a MessageConnection, which we then return to the caller. The helper method also sets a message listener for the connection, if one is specified. Note that setting the connection's message listener to null deregisters any current message listener, which will then not receive any notifications. I'll cover message listeners later in the article.
Although GCF provides convenience methods to create InputStream and OutputStream for StreamConnections, be aware that MessageConnection doesn't support stream-based operations. Attempting to create a *Stream will throw an IllegalArgumentException.
Closing a Message Connection

To close a connection, call the connection's close() method, as shown in the next code snippet:
/** * Closes the specified message connection * * @param connection is the connection to close */ static final public void closeConnection(MessageConnection connection) { try { if (connection != null) { connection.setMessageListener(null); // deregister the msg listener connection.close(); } } catch (IOException ioe) { // Handle or ignore the exception... } }
This method first deregisters any message listener and then closes the connection. Don't forget to call the above method to close connections and unset your message listeners during cleanup, in destroyApp().
Creating and Sending Messages

Recall that MessageConnection provides methods to create and send Messages. To create a message call the message factory method newMessage(), and to send a message use the method send(). Note that newMessage() will return a message even if the connection has been closed.
As you learned earlier, you can create either a client connection or a server connection, specifying your choice in the connection URL. A message created from a client connection has its destination address already set, taken from the connection URL passed when the client connection was created. A server connection's destination address, on the other hand, is not already set; you must set it explicitly before sending the message.
Let's define the helper method sendMessage() to send Messages. The method takes for parameters the MessageConnection to use, the actual Message to send — BinaryMessage, MultipartMessage or TextMessage - and the optional destination URL.
A non-null URL indicates that the caller wants to set the destination address before sending the message, as in the case of server connections. The method also checks whether the message can be sent, by calling the method numberOfSegments().
Our example follows good practice by sending the message on its own thread of execution, to avoid contention with system threads such as the main display thread. The helper method alertUser() is not shown; it's called here to warn the user if the message can't be sent. Note the different exceptions that could arise while sending the message:
/** * Sends a Message on the specified connection * * @param mc the MessageConnection * @param msg the message to send * @param url the destination address, typically used in server mode */ final public void sendMessage( final MessageConnection mc, final Message msg, final String url) { Thread th = new Thread() { public void run() { try { if (url!= null) msg.setAddress(url); int segcount = mc.numberOfSegments(msg); if (segcount == 0) { alertUser(UiConstants.TXT_SEGCOUNT); } else { mc.send(msg); } } catch(Exception e) { // Handle the Exception: // IOException if the message could not be sent // because there was a network failure or // if the connection was closed // IllegalArgumentException if the message was // incomplete or contained invalid information // This exception is also thrown if the payload // of the message exceeds the maximum length // for the given messaging protocol // InterruptedIOException if a timeout occurs // while trying to send the message or this // Connection object was closed during this // send operation // NullPointerException if the parameter is null // SecurityException if the application does not // have permission to send the message } } }; th.start(); }
Creating and Sending a Text Message

Now that we have covered the generic sendMessage() helper method, let's define a helper method to send a TextMessage.
/** * Sends a TextMessage on the specified connection * * @param mc the MessageConnection * @param msg the TextMessage to send * @param url the destination address, typically used in server mode */ final public void sendTextMessage( MessageConnection mc, TextMessage msg, String url) { sendMessage(mc, msg, url); }
The method assumes the TextMessage has already been created. Let's now define a more useful version of sendTextMessage(), one that takes an ordinary String for input, and creates and sends the TextMessage for us. Before sending, the method populates the outgoing TextMessage by calling the method setPayloadText().
/** * Sends a TextMessage on the specified connection * * @param mc the MessageConnection * @param msg the message to send * @param url the destination address, typically used in server mode */ final public void sendTextMessage( MessageConnection mc, String msg, String url) { TextMessage tmsg = null; tmsg = (TextMessage) mc.newMessage(MessageConnection.TEXT_MESSAGE); tmsg.setPayloadText(msg); sendTextMessage(mc, tmsg, url); }
Sending binary and multipart messages follows the same pattern.
Creating and Sending a Binary Message

Now let's define another helper method, one to send a BinaryMessage:
/** * Sends a BinaryMessage on the specified connection * * @param mc the MessageConnection * @param msg the Binary Message to send * @param url the destination address, typically used in server mode */ final public void sendBinaryMessage( MessageConnection mc, BinaryMessage msg, String url) { sendMessage(mc, msg, url); }
The method assumes the BinaryMessage has already been created. Let's now define a more useful version of sendBinaryMessage(), one that takes a byte array for input, and creates and sends the BinaryMessage for us. Before sending, the method populates the outgoing BinaryMessage by calling the method setPayloadData().
/** * Sends a BinaryMessage on the specified connection * * @param mc the MessageConnection * @param msg the byte[] message to send * @param url the destination address, typically used in server mode */ final public void sendBinaryMessage( MessageConnection mc, byte[] msg, String url) { BinaryMessage bmsg; bmsg = (BinaryMessage) mc.newMessage(MessageConnection.BINARY_MESSAGE); bmsg.setPayloadData(msg); sendMessage(mc, bmsg, url); }
Creating and Sending a Multipart Message

Let's define one more helper method, to send a MultipartMessage.
/** * Sends a multi-part message on the specified connection * * @param mc the MessageConnection * @param msg is the multiplart message to send * @param url the destination address, typically used in server mode */ final public void sendMultipartMessage( MessageConnection mc, MultipartMessage msg, String subject, String url) { sendMessage(mc, msg, url); }
The method assumes the MultipartMessage has already been created. As for text and binary messages, we'll define a more useful version of sendMultipartMessage(), one that takes an array of MessageParts for input, and creates and sends the MultipartMessage for us. Before sending, the method populates the outgoing MultipartMessage by adding to it each individual MessagePart. The method takes a lot of arguments: the MessageConnection; an array of MessageParts; the start-content ID; lists of TO, CC, and BCC identifiers; a message subject; the message's priority; and the destination URL.
/** * Sends a multi-part message on the specified connection * * @param mc the MessageConnection * @param msgParts the array of message parts to send * @param startContentID is the ID of the start multimedia * content part (SMIL) * @param to is the message's TO list * @param cc is the message's CC list * @param bcc is the message's BCC list * @param subject is the message's subject * @param priority is the message's priority * @param url is the destination URL, typically used in server mode */ final public void sendMultipartMessage( MessageConnection mc, MessagePart[] msgParts, String startContentID, String[] to, String[] cc, String[] bcc, String subject, String priority, String url) { try { int i=0; MultipartMessage multipartMessage; multipartMessage = (MultipartMessage) mc.newMessage(MessageConnection.MULTIPART_MESSAGE); if (to != null) { for (i=0; iFor multimedia messages with presentation information, such as those in theSynchronized Media Integration Language (SMIL) defined by the World Wide Web Consortium (W3C), the multipart message's start-content ID should always be set to point to the message part that contains the presentation information. You do so by calling the method setStartContentId(). You may set the start-content ID to null; if it's not null the reference must be to a valid message part, or an IllegalArgumentException will be thrown. The result of this call is that the MMS content type becomes application/vnd.wap.multipart.related, followed by a start parameter as shown next. Recipients of this type of message will then know how to retrieve the presentation information and present the message to the user appropriately.
[THE FOLLOWING ARE THE MMS HEADERS] X-Mms-Message-Type: m-send-req X-Mms-Transaction-ID: 123456 X-Mms-Version 1.0 X-Mms-Message-Class: Personal X-Mms-Expiry:36000 X-Mms-Priority: Normal From: +15121234567 To: +15121234544 Date: Fri, 17 May 2005 21:30:30 -0600 Subject: A multimedia message sample Content-Type: application/vnd.wap.multipart.related;start=; type=application/smil;Application-ID=com.j2medeveloper.MyMmsApp; Reply-To-Application-ID = com.j2medeveloper.MyMmsApp nEntries: 2 [THE FOLLOWING IS THE MMS BODY THAT CONSIST OF MESSAGE PARTS] [EACH MESSAGE PART CONSISTS OF TYPE, CONTENT ID, AND CONTENT] Content-Type: image/png; name="image1.png" content-id: <001> [...IMAGE DATA...] Content-Type: application/smil; name="first.sml" content-id:
Next let's look at MessageParts.
Creating Message Parts

As you've seen, MessageParts are at the center of MultipartMessages; each comprises a MIME type, a unique content ID, and the content itself.
You can construct a MessagePart from a byte array or a java.io.InputStream. Three constructors are available:
MessagePart(byte[] contents, int offset, int length, String mimeType, String contentId, String contentLocation, String encoding) throws SizeExceededException; MessagePart(byte[] contents, String mimeType, String contentId, String contentLocation, String encoding) throws SizeExceededException; MessagePart(java.io.InputStream contents, String mimeType, String contentId, String contentLocation, String encoding) throws IOException, SizeExceededException;
Where:
contents refers to the actual message content. mimeType is the MIME type, as defined in RFC2046. contentId is a required ID that uniquely identifies a message part, as defined in RFC2045. contentLocation specifies the filename for the attached message represented by the content. A null value means that no content location value is set for this message part. encoding identifies the content's encoding scheme. A null value means that no encoding is specified for this message part.
The first two constructors enable you to create the message part from a byte array, while the last constructor enables you to create the message part from an InputStream. A rule of thumb says that you should construct the message part from an InputStream instead of a byte[] if the content size is close to 10KB.
The following code snippet illustrates now to create two message parts, one text/plain and the other image/png.
public final static String MIMETYPE_TEXT_PLAIN = "text/plain"; public final static String MIMETYPE_IMAGE_PNG = "image/png"; ... MessagePart textMessagePart, imageMessagePart; try { // Create a text/plain part String textContent = "Hello world"; String textContentId = "text_hello"; String textContentLocation = "/helloworld.txt"; textMessagePart = new MessagePart( textContent.getBytes(), 0, textContent.length(), MIMETYPE_TEXT_PLAIN, textContentId, textContentLocation, null); // Create an image/png part InputStream imageContent; String imageContentId = "img_hello"; String imageContentLocation = "/helloworld.png"; imageContent = getClass().getResourceAsStream (imageContentLocation); imageMessagePart = new MessagePart( imageContent, MIMETYPE_IMAGE_PNG, imageContentId, imageContentLocation, null); } catch (SizeExceededException see) { // Handle exception } catch (IOException ioe) { // Handle exception }
Your code must handle the following exceptions:
IOException if an exception other than EOFException occurs while reading the InputStream. IllegalArgumentException if the specified MIME type is null. SizeExceededException if the content size is larger than the available memory or the size is not supported for the message part.
Once the individual message parts have been created, we are ready to send them using the sendMultipartMessage() helper method you saw earlier, that automatically creates and sends the MultipartMessage for us.
... String connUrl = ...; MessageConnection mc; MessagePart textMessagePart, imageMessagePart; MessagePart[] msgParts; ... mc = newMessageConnection(connUrl, null); ... // Create a message parts array textMessagePart = new MessagePart(...); imageMessagePart = new MessagePart(...); msgParts = new MessagePart[2]; msgParts[0] = textMessagePart; msgParts[1] = imageMessagePart; // Send the multiple parts sendMultipartMessage(mc, msgParts, connUrl); ...
Waiting for Incoming Messages

Recall that only a server-mode MessageConnection can receive messages. There are two approaches to receiving messages:
Synchronously, having a thread wait for incoming messages Asynchronously, having the system notify the application when new messages directed to the application have arrived
The approach to choose is really a matter of personal preference. I personally prefer the asynchronous approach, to avoid having a thread running even when there are no messages to process. This approach may be a bit more expensive on resources, as a new thread needs to be created and dispatched for each incoming message; but as the typical WMA use case is for messages that are not that frequent, the cost of dispatching one thread per message is acceptable.
Note that before a message is delivered to the application, the WMA implementation may request user confirmation.
Synchronous Messaging
For synchronous messaging you normally dispatch a thread during MIDlet initialization. This thread loops for messages, invoking Message.receive() to block while it waits for incoming messages, then consuming them as they become available. As an example let's define the class MySynchronousMsgReader, a Runnable that implements a Thread of execution to wait for and process incoming messages.
public class MySynchronousMsgReader implements Runnable { MessageConnection messageConnection; boolean done; ... /** * Constructor * * @param messageConnection is the MessageConnection to use */ public MySynchronousMsgReader( MessageConnection messageConnection) { this.messageConnection = messageConnection; Thread th = new Thread(this); th.start(); } ... /** * Sets the done flag to true */ public void setDone() { done = true; } /** * Message receive thread of execution */ public void run() { while (!done) { try { Message message; message = messageConnection.receive(); if (message != null) { processMessage(message); } } catch (IOException ioe) { // Handle or ignore the exception } } } /** * Process the message * * @param message is the Message to process */ public void processMessage(message) { // Here process the message } ... }
I'll discuss message processing shortly.Asynchronous Messaging
For asynchronous messaging WMA implements the conventional Observer or Listener design pattern for receiving messages asynchronously; that is, without blocking while waiting for messages. WMA defines the MessageListener interface, with a single method, notifyIncomingMessage(), which the platform invokes each time it receives a Message.
To use a message listener the application must implement the interface MessageListener and its notifyIncomingMessage() method, thus:
public class MyClass implements MessageListener { ... /** * Asynchronous callback for inbound message * Called by the WMA implementation when a new message is ready. * * @param connection is the message connection with incoming * messages. */ public void notifyIncomingMessage(MessageConnection connection) { readMessageThreaded(connection); } ... }
The message listener's notifyIncomingMessage() method receives the MessageConnection with inbound messages for input. Once this method has been invoked, the application must retrieve the new message by calling messageConnection.receive(). The following methods implement some helper methods to receive messages.
///////////////////////////////////////////////////////////////////// // Receive Helper Methods ///////////////////////////////////////////////////////////////////// /** * Thread of execution to read messages from MessageConnection * * @param messageConnection is the messageConnection with inbound * messages */ public void readMessageThreaded(final MessageConnection messageConnection) { Thread th = new Thread() { public void run() { readMessage(messageConnection); } }; th.start(); } /** * Reads a message from MessageConnection, processes the message * @param messageConnection is the messageConnection with inbound * messages */ public void readMessage(final MessageConnection messageConnection) { try { Message message = null; message = messageConnection.receive(); if (message != null) { processMessage(message); } } catch (IOException ioe) { System.out.println("readMessage exception: " + ioe); } } /** * Process the message * @param message is the Message to process */ public void processMessage(message) { // Here process the message }
Remember that, to enable asynchronous messaging, the application must register a message listener with the connection by calling MessageConnection.setListener(), as in the helper method newMessageConnection() you saw earlier. Also see that setting the connection's message listener to null deregisters any current message listener, which will then not receive any notifications.
... mc.setMessageListener(messageListener); ...
Note that the readMessageThreaded() method we just defined spawns its own thread of execution to read the message. Because the listener method is called by the platform, you must always minimize the processing it does on the system thread. You should always dispatch a separate thread of execution to consume and process the message, so the platform spends as little time as possible in notifyIncomingMessage().
Processing Messages

Once a message has been read by calling messageConnection.receive(), it's ready for processing. The return type of receive() is the generic supertype Message, so before you can process the message appropriately you must discover its subtype: BinaryMessage, MultipartMessage, or TextMessage.
Let's define a helper method that can be used to view WMA messages through MIDP's Liquid Crystal Display User Interface (LCDUI). It takes a Message for input and returns UI elements or Items the application can display on an LCDUI Form. The processMessage() method uses the instanceof operator to learn the Message's subtype, then calls the appropriate process-message method, which returns an array of javax.microedition.lcdui.Items.
/** * ProcessMessage processes a Message */ public Item[] processMessage(Message message) { Item[] items = null; // Process the received message appropriately to its type if (message instanceof TextMessage) { TextMessage tmsg = (TextMessage)message; items = processTextMessage(tmsg); } else if (message instanceof BinaryMessage) { BinaryMessage bmsg = (BinaryMessage)message; items = processBinaryMessage(bmsg); } else if (message instanceof MultipartMessage) { MultipartMessage mpmsg = (MultipartMessage)message; items = processMultipartMessage(mpmsg); } else { // Ignore System.out.println("Unrecognized message type..."); return null; } return items; }
This method delegates the actual work of processing the message and returning the array of javax.microedition.lcdui.Items to one of three helpers. One of these, processTextMessage(), retrieves a text message's payload by calling TextMessage.getPayloadText(), then returns a one-element StringItem array that contains the message's text:
/** * Process TextMessage * * @param tmsg is the TextMessage to process */ public Item[] processTextMessage(TextMessage tmsg) { Item[] items; String text = tmsg.getPayloadText(); items = new Item[1]; Item item = new StringItem(null, text); items[0] = item; return items; }
The processBinaryMessage() helper method is similar, but instead of processing a text message it retrieves a binary message's payload by calling BinaryMessage.getPayloadData(), converts the byte array to a hexadecimal string, then returns a one-element StringItem array that contains the hex:
/** * Process BinaryMessage * * @param bmsg is the BinaryMessage to process */ public Item[] processBinaryMessage(BinaryMessage bmsg) { Item[] items; byte[] data = bmsg.getPayloadData(); String hex = toHex(data); items = new Item[1]; Item item = new StringItem(null, hex); items[0] = item; return items; } /** * toHex converts a byte array to human-readable hexadecimal * This method was borrowed from examples in the Sun Wireless * Toolkit :-) * @param data is the array of bytes to convert * @return a String containing the byte[] in hexadecimal format */ final private String toHex(byte[] data) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < data.length; i++) { int intData = (int)data[i] & 0xFF; if (intData < 0x10) buf.append("0"); // display 2 digits per byte i.e. 09 buf.append(Integer.toHexString(intData)); buf.append(' '); } return (buf.toString()); }
The next helper method, processMultipartMessage() is a bit more complex.
The MMS headers in a MultipartMessage are used to ensure delivery of the message from a sender to a recipient, and contain information such as TO, CC, BCC, and FROM addresses. The MMS body itself comprises one or more message parts, each consisting of headers, type, and body.
Each individual message part can be of any MIME type, but the message's overall MMS content type is one of three:
application/vnd.wap.mms-message if it is not a true multimedia message application/vnd.wap.multipart.related if it is a multimedia message with related presentation information application/vnd.wap.multipart.mixed if it is a multimedia message with unrelated parts
The following helper method processMultipartMessage() retrieves the MultipartMessage header information, then each message part, and finally returns an array of Items that correspond to each part's MIME type.
... // MIME Types public final static String MIMETYPE_TEXT_PLAIN = "text/plain"; public final static String MIMETYPE_TEXT_XML = "text/xml"; public final static String MIMETYPE_TEXT_HTML = "text/html"; public final static String MIMETYPE_IMAGE_PNG = "image/png"; public final static String MIMETYPE_IMAGE_JPEG = "image/jpg"; public final static String MIMETYPE_IMAGE_GIF = "image/gif"; public final static String MIMETYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"; ... /** * Process MultipartMessage * * @param mpmsg is the MultipartMessageto process * @return array of javax.microedition.lcdui.Item */ public Item[] processMultipartMessage(MultipartMessage mpmsg) { // Get the Multipart message headers from the access methods: // X-Mms-From, X-Mms-To, X-Mms-CC, X-Mms-BCC, X-Mms-Subject String from = mpmsg.getAddress(); String tos[] = mpmsg.getAddresses("to"); String ccs[] = mpmsg.getAddresses("cc"); String bccs[] = mpmsg.getAddresses("bcc"); String froms[] = mpmsg.getAddresses("from"); String subject = mpmsg.getSubject(); // Get the rest of the headers by calling the getHeader() method String mmsPriorityHeader = mpmsg.getHeader("X-Mms-Priority"); String mmsDeliveryTimeHeader = mpmsg.getHeader("X-Mms-Delivery-Time"); // Get the message timestamp Date timestamp = mpmsg.getTimestamp(); // Get the start-content ID, which will be set for multimedia // content, and will contain the presentation information // (not used in this method) String startContentID = mpmsg.getStartContentId(); // Get to the message parts array MessagePart[] messageParts = mpmsg.getMessageParts(); int itemCount = messageParts.length + 2; // +2 is to account for from & subject Item[] messageItems = new Item[messageParts.length]; // Create the UI elements for the message's Date and Subject messageItems[0] = new StringItem("From", from); messageItems[1] = new StringItem("Subject", subject); // process each message part for (int i=0; iNote how some of the MultipartMessage information that's stored as RFC822 headers is accessible through access methods, while other headers are available only by calling getHeader() to retrieve them directly,. Other attributes are inaccessible for security reasons.
One of the headers, the start-content ID, refers to the message part that contains the presentation information for true multimedia messages, such as SMIL-based content. To retrieve the start message content ID, call the multipart message method getStartContentId(), as here:
... // Get the start-content ID, which will be set for multimedia // content, and will contain the presentation information String startContentID = mpmsg.getStartContentId(); ...
The format of the X-Mms-Delivery-Time header is milliseconds since midnight January 1, 1970 UTC, as a string value. The format of the contents of X-Mms-Priority is a string with the value "high", "normal", or "low".
... // Get the Multipart message headers via the access methods: // X-Mms-From, X-Mms-To, X-Mms-CC, X-Mms-BCC, X-Mms-Subject String from = mpmsg.getAddress(); String tos[] = mpmsg.getAddresses("to"); String ccs[] = mpmsg.getAddresses("cc"); String bccs[] = mpmsg.getAddresses("bcc"); String froms[] = mpmsg.getAddresses("from"); String subject = mpmsg.getSubject(); // Get the rest of the headers by calling the getHeader() method String mmsPriorityHeader = mpmsg.getHeader("X-Mms-Priority"); String mmsDeliveryTimeHeader = mpmsg.getHeader("X-Mms-Delivery-Time"); // Get the message timestamp Date timestamp = mpmsg.getTimestamp(); ...
The method getAddresses() takes for input the address type "to", "cc", "bcc", or "from", and returns a String array, or null if the specified address type is not found. There can be one or more TO, CC, and BCC addresses in any combination. The getHeader() method takes the header name for input. It throws a SecurityException if the requested header is restricted, or an IllegalArgumentException if the header is unknown. For more information on MMS headers refer to Appendix D of the WMA 2.0 specification.
Message Persistence

Incoming WMA messages are initially stored on the device's Subscriber Identity Module (SIM) card, and removed from the SIM as they're delivered to the application. Message persistence at the application level is the developer's responsibility. To add message persistence to your application you must serialize and deserialize the message payloads, and use the Record Management System (RMS) API to manage a local store.
About Segmentation and Reassembly

Some transports impose a limit on the size of a single message or on the number of segments that compose a message. Segmentation and reassembly (SAR) is a feature of some low-level transports that concerns breaking a large message into a number of smaller sequential, ordered segments, or transmission units. For example, a message sent on the SMS network is typically limited to 160 GSM 7-bit encoded characters or 140 binary 8-bit bytes; to send a longer message you must segment it. Reassembly is the opposite of segmentation: putting those smaller, related segments back together into a single message.
The WMA 2.0 specification mandates that WMA-based SMS implementations must support at least three SMS-protocol segments for a single message. Some implementations may support more, but applications will be more portable if they adhere to the three-segment limit, and refrain from sending messages that are larger than 456 GSM 7-bit encoded characters, 198 double-byte characters, or 399 binary bytes; see WMA 2.0 Appendix A.1.2 "Message Payload Concatenation" for more information. Sending a larger message may result in an IOException. Note that for MMS messages, SAR is handled differently, and the size limitation is not as restrictive. Individual message parts can be kilobytes in size. A SizeExceededException will result if an MMS message part is larger than the available memory, or than the supported size.
To help you deal with SMS message segmentation, WMA provides a method called MessageConnection.numberOfSegments(), which provides segmentation information for a Message before you send it with MessageConnection.send(). The numberOfSegments() method returns the number of segments it will take to send a message, or 0 if the message can't be sent. Let's look at a modified version of sendTextMessage() that uses numberOfSegments():
... Message msg = ...; int segcount = mc.numberOfSegments(msg); if (segcount == 0) { // can't send, alert the user alertUser(SEGMENTATIONERROR); } ...
With the information you get from numberOfSegments() you can also warn the user about higher costs of sending longer messages. For example, sending a short message might cost 5 cents, while sending a large message in three segments might cost 15 cents. Users who want to control costs will welcome the opportunity to cancel longer messages.
System Properties

The WMA 2.0 defines two system properties to retrieve the address of the Short Message Service Center (SMSC) and the Multimedia Message Service Center (MMSC):
wireless.messaging.sms.smsc - the SMSC attribute name wireless.messaging.mms.mmsc - the MMSC attribute name
Use the method System.getProperty(String attributeName) to retrieve these values, thus:
... String smsc = System.getProperty("wireless.messaging.sms.smsc"); String mmsc = System.getProperty("wireless.messaging.mms.mmsc"); ...
The SMSC address is an MSISDN number such as +18472549270. The MMSC address is an MSISDN number value like +18472549270 or a URL like http://mmsc.cingular.com.
WMA 2.0 and the Push Registry

MIDP 2.0's push registry enables MIDlets to set themselves up to be launched automatically, without user initiation. The push registry manages network- and timer-initiated MIDlet activation; that is, it enables an inbound network connection or a timer-based alarm to wake a MIDlet up.
MIDlets can be activated by incoming WMA connections, if the underlying implementation provides the necessary support. Before a MIDlet can be activated, it must register with the push registry using either static or dynamic registration. Static registration is done by placing MIDlet-Push-n entries in the JAD or MANIFEST file; dynamic registration is done at runtime, using the PushRegistry object's registerConnection() API.
Here's an example of using static registration:
... MIDlet-Push-1: sms://:50000, MyWmaMIDlet, +123456789 MIDlet-Push-2: cbs://:50001, MyWmaMIDlet, * MIDlet-Push-3: mms://:com.j2medeveloper.MyMmsApp, MyWmaMIDlet, * ...
The first attribute value is the local (server) connection URL; that is, the server protocol and port number or application ID. The second value is the name of the MIDlet class responsible for handling incoming messages for the specified connection URL, and the third is a filter used to restrict the senders that can activate the MIDlet. The next code snippet shows how to use dynamic registration:
... // Identify the MIDlet class String midletClassName = this.getClass().getName(); // Register a static connection String url = "sms://:50000"; // Use a filter for SMS String filter = "+123456789"; // only allow messages from +123456789 ... try { PushRegistry.registerConnection(url, midletClassName, filter); } catch (IOException ioe) { // Handle the exception } catch (ClassNotFoundException ioe) { // Handle the exception } ...
Because the will maintain registrations across MIDlet invocations, it is important to unregister the push connection when it's no longer needed by calling the method unregisterConnection():
... String url = "sms://:50000"; ... try { // unregisterConnection returns true if successful, false if not status = PushRegistry.unregisterConnection(url); } catch(SecurityException e) { // Handle the exception } ...
You can discover whether your MIDlet was activated by the by calling the method listConnections(). Let's define the helper method processPushConnections() to discover and process any pushed connections. You typically invoke this method during application startup.
/** * Discover whether there are pending push inbound connections * and, if so, process */ public void processPushConnectionsThreaded() { Thread th = new Thread() { public void run() { String[] connections = PushRegistry.listConnections(true); if (connections != null && connections.length > 0) { for (int i=0; iNote that the processPushConnectionsThreaded() method runs on its own thread of execution, for two reasons: to avoid contentions with the system threads, and to serialize message processing so messages are processed in the same order they were received. "The MIDP 2.0 Push Registry" has more information on how to use the push registry's API.
Security

WMA does not define a security mechanism, but instead uses the underlying platform's security framework. On some platforms, including MIDP 2.0, networking operations are considered privileged operations < meaning that to open a connection or to send and receive messages, permissions must be requested by the application and granted by the platform; specifics are implementation-dependent. In MIDP 2.0, permissions are requested by way of the JAD or the manifest, and granted (or not) by the user when the operation (in our case open, send, or receive) is invoked. Note that for signed MIDlets, permissions must be defined in the manifest.
Here's an example of requesting WMA permissions via the JAD or manifest:
... MIDlet-Permissions: javax.microedition.io.Connector.sms, javax.wireless.messaging.sms.receive, javax.wireless.messaging.sms.send, javax.microedition.io.Connector.cbs, javax.wireless.messaging.cbs.receive, javax.microedition.io.Connector.mms, javax.wireless.messaging.mms.receive, javax.wireless.messaging.mms.send, javax.microedition.io.PushRegistry ...
The WMA defines the SecurityException class to notify the application of permission exceptions it encounters when invoking platform services. This exception can be thrown as follows:
javax.microedition.io.Connector: if the application is not granted permission to create a connection for a given messaging protocol, as defined by the platform security services MessageConnection.send(): if the application has no permission to send messages on the specified port MessageConnection.receive(): if the application has no permission to receive messages on the specified port
For more information about MIDP 2.0 permissions, see the MIDP 2.0 specification and the WMA Recommended Practices, which you can find on theWMA technology page.
The WMA v2.0 Reference Implementation

You can find a reference implementation of the Wireless Messaging API 2.0 that you can use to test your programs in theJ2ME Wireless Toolkit technology page.
About Server-to-Handset Messaging

This article has covered how to send "mobile-originated" messages from the handset, and how to receive "mobile-terminated" WMA messages sent from a handset or a server. To send messages to a handset from a server you need access to the network provider's SMSC and MMSC servers. Access to these servers is considered privileged and typically you need to go through third-party messaging vendors. These vendors already have relationships with the major carriers such as AT&T (Cingular Blue), Boost Mobile, Cingular (Orange), Nextel, T-Mobile, Sprint, and Verizon, as well as with second-tier carriers such as Alltel, Western Wireless, Leap, Cincinnati Bell, and Dobson. These vendors provide access to the carrier's SMSC and MMSC through exposed APIs that use HTTP and other protocols, allowing your server application to send messages to given handsets, receive messages from handsets, and use premium services such as short-codes.
Also remember that, if you need to send a message to a particular application on a handset, you must set the appropriate port or application ID for the message; if you don't, the message will be delivered to the handset's default viewer. How you set port number or application ID on an outgoing message will depend on the messaging service you use; the vendor may provide APIs or you may have to build the raw message in binary form. For GSM SMS, the port number resides in the TP-User-Data/User-Data-Header field, while for MMS the application ID resides in the MMS Content-type header.
Summary

The Wireless Messaging API provides a very flexible messaging API that is protocol- and device-independent. The reference implementation allows you to start writing and testing WMA-based applications today. WMA's flexible design ensures that it can easily be extended in the future, and it already supports the most common message types: text, binary, and multimedia. The WMA allows developers to create neat applications that incorporate messaging, such as SMS, email, and rich multimedia, as well as push behavior and consumption of short messages.
References

WMA 2.0 SpecificationMIDP 2.0 SpecificationGSM 03.40 v7.4.0, ETSI Digital Cellular Telecommunication SystemsGSM 03.41 v7.3.0, ETSI Digital Cellular Telecommunication SystemsWAP-209-MMSEncapsulationRFC822 Standard for the format of ARPA Internet TextRFC2045 Multipurpose Internet Mail Extensions (MIME)RFC2046 Multipurpose Internet Mail Extensions (MIME)RFC2387 The MIME Multipart/Related Content-type
Acknowledgments

I would like to thank Gary Adams of Sun Microsystems, for his feedback to this article.
About the Author

C. Enrique Ortiz is a software architect and developer, and a wireless mobility technologist and writer. He is author or co-author of manypublications, and has been an active participant in the wireless Java community and in various J2ME expert groups. Enrique holds a B.S. in Computer Science from the University of Puerto Rico and has more than 15 years of software engineering, product development, and