Exchange Web Services Example – Part 4 – Subscriptions and Notifications

Now that we have succeeded in creating, updating and deleting calendar items and mastered Exchange Impersonation, it’s time to turn our attention to having Exchange notify us about what it is doing. Part 4 of this series is going to provide a detailed code walk-through of some code that leverages the Subscription API.

I covered a lot of material about the push subscription mechanism in an earlier post, and if you haven’t read it, I highly recommend you go back and revisit it as this article builds on that one. It also builds on part 1, part 2, and part 3 of this series, so you should probably have worked through those, too.

Goals and Background

By now, you know that the reason that I have embarked on this project is to integrate Microsoft Exchange with a CRM application that is in Progress OpenEdge. The first part of the exercise was to figure out how to create calendar items based on data entered into the CRM system, and how to automate that for a large number of sales people without having to log in to each mailbox.

Now that we know how to do that, the next step is handling the situation where the sales person makes a change to a calendar item from Outlook or a mobile device. The item change needs to make it back to the CRM application, and for that, the Exchange WebServices Push Subscription API is going to be essential.

When a change is made to an Exchange Server calendar item, the Push Subscription API can send a message to an HTTP target of your choice. As long as the HTTP target knows how to parse the SOAP message that is sent by Exchange, it can react to the events that it are sent.

In this example, we are going to concentrate on 3 events that occur against objects in the default calendar folder for a mailbox; the Created-, Modified-, and DeletedEvents.

The Subscription Process

The Subscription processBy now, the diagram to the left is all too familiar, but it clarifies the process really well, so I am going to walk through it again.

The first step (1.1) is to initiate a call to the Java EWS WebService to register a subscription. The EWS WebService then makes a call to Exchange (1.2) to tell it to call back to the Java HTTP Servlet.

When something happens to an item for which we have registered a subscription, Exchange will call the HTTP Servlet (2.1) with a notification message.

Now the order of what happens next is something that you will want to change for production code, because it should happen in the order illustrated in the diagram, but I have decided to take the simple road with the attached code and process everything in a synchronous way. What follows is the order in which I am doing the processing in the sample code.

After parsing the message notification message that was received (2.1), the code determines if there are any items that need to be processed. A notification message only contains the ItemID of the object that has changed. It does not contain the item itself. This means that we need to call the Exchange Server back to get the item’s information (2.3).

Once we have the item’s information, we can react to it, and in the example code, this includes calling OpenEdge (2.4) with information about the appointment that has changed. It is really important to note that only notifications that actually affect an item are passed on to OpenEdge. This significantly reduces the processing that OpenEdge has to perform.

Finally, when we are done with the processing, we respond to Exchange with an acknowledgement message (2.2) so that it will continue to send us messages.

If you want more information about the details of this process, refer to my detailed description in the blog posting entitled Exchange Web Services – Subscriptions and Notifications.

It is important to remember that Exchange will poll the Java HTTP Servlet at regular intervals to check that it still wants to receive notifications. The frequency is user-defined as explained in the aforementioned article. These StatusEvents do not require any processing, other than an acknowledgement of receipt back to the Exchange Server (2.2). So the Java HTTP Servlet also performs the function of keeping the subscription alive.

I have also provided a mechanism for unsubscribing from the notifications. This mechanism works by keeping track of all the subscriptions that are currently active, and then returning an Unsubscribe response (2.2) to the Exchange Server after a request to unsubscribe has been received in 1.1.

The Code

Now that you understand how the process works, let’s take a look at the code.

First, I need to point out that the download contains 2 zip files. If you are a Java developer with no interest in OpenEdge, you should use the file called EWSAPI_JavaOnly.zip. The difference between this code and the EWSAPI_OpenEdge.zip file is that the latter actually makes AppServer calls to an OpenEdge AppServer, whereas the former simply displays the results of processing the messages in the log file.

The EWSAPI_OpenEdge.zip files can also be installed per the regular instructions, but there are a few additional steps that you have to perform. These are documented in a file called OpenEdge_Setup.pdf that is contained in the downloaded zip file. Essentially, you will need to create a database using a definition that is provided, set up an OpenEdge AppServer that has access to the database, and deploy some OpenEdge ABL code to the AppServer so that it can receive the calls. You will also need to copy your OpenClient runtime libraries to the Glassfish installation so that it can make the OpenClient calls. As I said, all of this is covered in OpenEdge_Setup.pdf.

Like the previous articles, you can download the code and set up your workspace as described in part 2.

What’s New?

The EWSAPI project that is included in both the EWSAPI_JavaOnly.zip and the EWSAPI_OpenEdge.zip files contains the following code changes in the com.tsg.ews root package (filenames in italics are new files):

  • EWSListener.java – This code, which is new, contains the Java code for the Java HTTP Servlet that listens for notifications from Exchange.
  • EWSImpersonationCalendar.java – This code, which is the code for the Impersonation-based calendar WebService, has been modified to support two new operations – Subscribe and Unsubscribe.

In addition, the com.tsg.ews.calendar package contains the following code changes:

  • Appointment.java – A minor modification has been made to the retrieve method to support retrieval with a ChangeKey. I also changed the code to use namespaces.

Finally, the com.tsg.ews.service package contains a lot of code changes (again, filenames in italics are new files):

  • EWSResponse.java – This code, which deserializes responses from the Exchange Web Service, has been altered to support namespaces in the deserialization of the XML. It also now supports a SubscriptionID and a Watermark property.
  • ExchangeService.java – There are a couple of minor changes to expose some methods outside of the class.

An enumeration and a new class have been added to enhance some of the XML parsing that I have been doing:

  • EWSNamespaces.java – This is an enumeration of the namespaces used in Exchange WebServices messages.
  • NamespaceContextImpl.java – This class extends NamespaceContext and is used in namespace resolution of the XPath queries. It makes use of EWSNamespaces to perform this resolution.

The following three classes have been added to support the registration and long-term monitoring of subscriptions:

  • SubscriptionManager.java – This class manages all subscriptions.
  • SubcriptionList.java – This class is a collection of subscriptions and is used for serialization and deserialization of all subscriptions.
  • Subscription.java – This class is used to register a subscription and serialize it and it keeps track of the watermarks for the subscription.

The remaining new classes and enumerations have been added to parse notifications from Exchange Server:

  • Notification.java – This class is the parent class for each notification. It can be instantiated from an Exchange Web Service SOAP Notification message and it will deserialize the message into its internal objects. The notification, itself, has properties for the SubscriptionId, PreviousWatermark, and a collection of SubscriptionEvents. It also has a flag that indicates whether it is just a status event.
  • SubscriptionEvent.java – A Notification may include one or more SubscriptionEvent objects. These events will be one of the EventTypes (see the next bullet point) for which the subscription has been registered (CreatedEvent, ModifiedEvent, DeletedEvent, or StatusEvent). After the event has been parsed, it contains properties for EventType, SubscriptionID, Watermark (each event will have a different watermark), TimeStamp, AffectedItem (the item that actually changed), and ParentFolder (the folder that contains the item that changed). If the AffectedItem is a calendar item, the calendar item will be retrieved (if possible) and stored in the Appointment property.
  • EventType.java – This is an enumeration of the supported event types.
  • IDHolder.java – When we receive an event, it comes with two elements; the first is either a FolderId or an ItemId, and the second is a ParentFolderId. These nodes contain two attributes; an ID and a ChangeKey. The IDHolder has three properties, two of which map to the ID and the ChangeKey. The third is an IDType, described in the next bullet.
  • IDType.java – This enumeration contains items for each possible type of ID that can be contained in the IDHolder.

The OpenEdge pieces

If you are using the OpenEdge version of the code (EWSAPI_OpenEdge.zip), the EWSAPI project contains the following additional packages:

  • com.tsg.common.collections
  • com.tsg.common.interfaces
  • com.tsg.dynamicopenclient
  • com.tsg.dynamicopenclient.data
  • com.tsg.dynamicopenclient.valueholders
  • com.tsg.exceptions
  • com.tsg.ews.openedge

All but the last package are modified copies of the code that accompanies the OpenEdge Dynamic OpenClient Java Example article that I wrote back in November 2009.

The last package contains one class, OpenEdgeCallFactory, that is responsible for making the OpenClient call to the AppServer.

The EWSAPI project for OpenEdge also contains an additional folder, oepieces, that contains the ABL procedure (ewscall.p) that it expects to call and a DF file (test.df) that contains the database definitions for the database table that ewscall.p expects to write to.

I’m not planning to cover the OpenEdge part of this in any detail later in the article. All ewscall.p does is accept a set of parameters and write their values directly to a table in an OpenEdge database. The ewscall.p procedure needs to be deployed to an OpenEdge AppServer. The AppServer needs to be running in STATE-FREE mode against a database that contains the ews_update table that is defined in the test.df file.

Additional Setup

Updating the soapUI WebService DefinitionOnce you have set up the code and have it running, you need to update the WebService definition for the CalendarService in soapUI. The screenshot at left should help you do this. Right-click on the EWSImpersonationCalendarPortBinding tree node, and select Update Definition from the context menu.

Two additional nodes will be added – Subscribe and Unsubscribe. As you will note from the screenshot, I have created two copies of the RegisterServer request and 8 copies of the Subscribe request.

I have two Exchange servers – One running Exchange 2010 and one running Exchange 2007 SP3. Each Exchange Server has 4 mailboxes that I am using for testing, hence 2 RegisterServer requests and 8 Subscribe requests.

One other thing I would recommend is if you have an MSDN subscription and you can install Outlook 2010, do so. It will make your life a whole lot easier for testing. In Outlook 2010, you can add more than one Exchange mailbox to your list of mailboxes which means that you can work on any of the test mailboxes from just one Windows login. This makes testing much, much easier.

Establishing a Subscription

As I have described, there are two parts to the Subscription API – the Subscription and the Notification. This code walk-through is therefore going to be divided into two main sections covering each of those parts of the API. This section covers setting up the subscription, and the subscription starts at the CalendarService WebService which is in the EWSImpersonationCalendar class in com.tsg.ews.

EWSImpersonationCalendar

This code was included in part 3 of this series, but we have added two additional operations in Lines 76 through 98; Subscribe and Unsubscribe:

@WebMethod(operationName = "Subscribe")
@WebResult(name = "Response")
public EWSResponse subscribe(@WebParam(name = "ServerID") String serverID,
				 @WebParam(name = "UserAccount") String account,
				 @WebParam(name = "CallbackURL") String url,
				 @WebParam(name = "OpenEdgeURL") String oeUrl) {
	Subscription sub = new Subscription(serverID);
	sub.setAccount(account);
	sub.setListenerURL(url);
	sub.setOpenedgeURL(oeUrl);
	EWSResponse resp = sub.subscribe();
	if (!resp.hasError()) {
		SubscriptionManager.getInstance().register(sub);
	}
	return resp;
}

@WebMethod(operationName = "Unsubscribe")
@WebResult(name = "Response")
public EWSResponse unsubscribe(@WebParam(name = "SubscriptionID") String subscriptionID) {
	SubscriptionManager.getInstance().unsubscribe(subscriptionID);
	return new EWSResponse("NoError", 0, "Success");
}

These two operations rely heavily on the concept of the SubscriptionManager which keeps track of all active subscriptions and ensures that they are persisted to a local XML file. This means that when a notification is received later, we have the ability to relate that notification back to the subscription that initiated it, even if we shutdown and restart the Glassfish server. We will definitely talk more about this later once you have a chance to understand the concepts that affect it.

Looking at the Subscribe operation/method in lines 76 through 91, the operation expects to receive a ServerID as an input parameter. This ServerID is the ServerID we retrieved from a previous RegisterServer call. The UserAccount is used for Exchange Impersonation and to specify the mailbox for which the subscription is to be registered. The CallbackURL is the URL of the Java HTTP Servlet that will handle the notifications, and the OpenEdgeURL is the URL that should be used to call the OpenEdge AppServer.

Lines 82 through 85 establish a Subscription object and in line 86, the subscribe method is called. We’ll look at the subscribe method in a little more detail in a moment, but for now just accept that it is responsible for actually making the call to the Exchange WebService and obtaining a SubscriptionID. In production code, the Subscription object would be instantiated by the SubscriptionManager, but it makes for more readable example code to do it the way I have done it here.

Lines 87 and 88 check the error status on the EWSResponse object and register the subscription with the SubscriptionManager if the subscription was successful. Whether the subscription was successful or not, the EWSResponse object is returned from the WebService, and if it was successful, it contains the SubscriptionID and Watermark for the subscription.

The Unsubscribe operation in lines 93 through 98 pass the SubscriptionID to the unsubscribe method on the SubscriptionManager.

There is a special case that the Unsubscribe method will support: If an “*” is passed as the SubscriptionID, all existing subscriptions will be canceled.

The Subscription Object

I said we would take a closer look at the subscribe method, and we’ll do that in just a moment. First, though, open the Subscription.java class that is in the com.tsg.ews.service package. For the most part, this object is a set of properties, including the ServiceID, ListenerURL, OpenEdgeURL, Account, SubscriptionID and Watermark. The class has JAXB serialization annotations associated with it so that it can be serialized to and from XML.

There is also a canceled flag. The canceled flag is set to false until the cancel method in line 130 is called.

The subscribe method is in lines 97 through 128. The SOAP message that needs to be sent is built between lines 98 through 118:

public EWSResponse subscribe() {
	StringBuffer request = new StringBuffer();
        request.append("<Subscribe \n");
        request.append("               xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\"\n");
        request.append("               xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\n");
        request.append("    <PushSubscriptionRequest>\n");
        request.append("\n");
        request.append("        <t:FolderIds>\n");
        request.append("            <t:DistinguishedFolderId Id=\"calendar\"/>");
        request.append("        </t:FolderIds>\n");
        request.append("        <t:EventTypes>\n");
        request.append("            <t:EventType>CreatedEvent</t:EventType>\n");
        request.append("            <t:EventType>ModifiedEvent</t:EventType>\n");
        request.append("            <t:EventType>DeletedEvent</t:EventType>\n");
        request.append("        </t:EventTypes>\n");
        request.append("        <t:StatusFrequency>3</t:StatusFrequency>\n");
        request.append("        <t:URL>");
        request.append(this.getListenerURL());
        request.append("</t:URL>\n");
        request.append("    </PushSubscriptionRequest>\n");
        request.append("</Subscribe>\n");
        SOAPRequest message = new SOAPRequest(request.toString(), this.getAccount());

The Subscription message has been discussed in some detail in my previous post on Subscriptions and Notifications, and this message is largely the same message as that one, with the exception that the user can specify the URL, and the instantiation of the SOAPRequest object in line 118 (the last line) results in the use of Exchange Impersonation to make the request.

The DistinguishedFolderId node that is added in line 105 will result in notifications being generated based on any changes to the folder(s) that is(are) listed – in this case, just the calendar folder. The following folders are supported by name:

  • calendar
  • contacts
  • deleteditems
  • drafts
  • inbox
  • journal
  • notes
  • outbox
  • sentitems
  • tasks
  • msgfolderroot
  • root
  • junkemail
  • searchfolders
  • voicemail

Any folder can be monitored, but if it is not one of these folders, you need to supply the FolderID for the folder.

The EventTypes node in lines 107 through 111 defines the list of events for which notifications should be received. This list could include any of the following values:

  • CopiedEvent
  • CreatedEvent
  • DeletedEvent
  • ModifiedEvent
  • MovedEvent
  • NewMailEvent
  • FreeBusyChangedEvent (Exchange 2010 only)

The StatusFrequency node is described in a lot more detail in my previous post. In this case, I am asking for Exchange to send a status event to the Java HTTP Servlet every 3 minutes. This gives me the opportunity to cancel the subscription every 3 minutes. I have mixed feelings about status frequencies in terms of how long they should be, and I’ll talk more about this later in this article.

Once the message is built, we call makeRequest on the ExchangeService object with the message in lines 119 through 128:

        EWSResponse response;
        try {
		response = getService().makeRequest(message);
		subscriptionID = response.getSubscriptionId();
		watermark = response.getWatermark();
	} catch (Exception e) {
		response = new EWSResponse(e);
	} 
	return response;
}

If the call is successful, the EWSResponse object will contain a unique SubscriptionID and a Watermark. It is important to understand that the SubscriptionID is unique, even if you have already registered a subscription for this mailbox. In other words, it is possible to have more than one subscription active for a mailbox.

The SubscriptionManager Object

The SubscriptionManager object is responsible for tracking all active subscriptions. In essence, it is a persistence mechanism for maintaining the state of a subscription over a long period of time. The SubscriptionManager takes care of persisting the subscription details to an XML file and providing a central point through which all changes to a subscription can take place. Any time any object needs information about a subscription, it makes a call to the SubscriptionManager to obtain information about the subscription.

Any changes made to a subscription are persisted to the XML file, so that if the Glassfish server is restarted, and the subscription is not lost, the SubscriptionManager will be able to maintain the context of any subscriptions that are currently active.

Why do we care about this context anyway? For a number of reasons:

  1. We associate the OpenEdge URL with a subscription and there is no way to include this information with the information that we submit to Exchange Server so there is no way to know what we need to do when we receive a notification if we don’t persist the context of the subscription;
  2. To be able to specifically cancel a subscription, the SubscriptionManager needs to know about it. Remember that the only ways to cancel a subscription are by responding to a notification with an unsubscribe response, or by not responding and waiting for it to time out. Ideally, we need the ability to do this gracefully, and that means responding to a notification with the unsubscribe response. It may be several minutes between the time that an unsubscribe request is received by the Java EWS WebService and the time a notification is received by the Java HTTP Servlet, so all the SubscriptionManager can do is note that the subscription is canceled. When the next notification comes in, the Java HTTP Servlet can then act on the cancellation;
  3. In a production environment where there may be numerous mailboxes that are all being monitored concurrently, it would be far more efficient to receive all notifications for one mailbox as a set. The problem is that when a subscription is registered, there may be no way of knowing what other subscriptions will be registered for the same mailbox. Although the implementation that I have included does not do this, the SubscriptionManager is the ideal place to maintain a link between subscriptions for the same mailbox so that the load on the Exchange Server can be minimized and delegated to a separate middleware service; and
  4. If the Glassfish server goes down for any reason, context needs to be maintained so that when it is restarted, it can re-establish the subscriptions as they were running before it was terminated. Of course, in a production environment with load balancing in place, it would be unlikely that the Glassfish server farm would not be available, but the notion of more than one Glassfish server immediately means that the subscription context needs to be persisted centrally.

So although the SubscriptionManager that is provided with this example is somewhat primitive, it performs a key role in maintaining continuity and performance for subscriptions.

The SubscriptionManager code is not particularly interesting so I am not going to spend any time walking through it. It definitely has holes in it, but it works for the examples that we are using here.

Establishing a Subscription

Now that you know how the code works for establishing a subscription, let’s go ahead and register a subscription for one of the mailboxes. Assuming you already have the code installed and running in Eclipse and the Glassfish server running, open soapUI and open a request under the Subscribe operation for the CalendarService:

Creating the subscription request

The ServerID element should contain a UUID for a server that was registered using the RegisterServer operation. The UserAccount should contain the e-mail address of the mailbox that you want to monitor.

The CallbackURL is the first new element you need to complete. This is the URL that the Exchange Server should use for all notifications. This URL will be in the form http://<server-name>:8080/EWSAPI/EWSListener where <server-name> is the machine that is running your Glassfish server (your local machine). Note that you cannot use localhost as the server name. Calls to this URL are going to come from your Exchange Server so it needs a DNS-registered name.

The OpenEdgeURL is only of interest to you if you are an OpenEdge developer. If this element contains anything but a string that begins with AppServer:// it will be ignored. If you are using the default asbroker1 AppService, the URL that I have in the screenshot (AppServer://localhost:5162/asbroker1) should work for you, assuming your AppServer is running on the same machine as your Glassfish server.

Once you have filled out the request parameters, submitting the request will get you a response similar to the one in the following screenshot:

Subscription request response

The Response node contains 3 attributes. The ErrorState attribute should have a value of “NoError” and should be followed by a SubscriptionID attribute and a Watermark attribute, both with strings of characters that don’t mean anything. The SubscriptionID uniquely identifies the subscription and can be used to unsubscribe through the Unsubscribe operation.

If you switch your focus to the Console view in Eclipse for the Glassfish Server Log, you will see messages start popping up occasionally in the view like the one below:

INFO: Notification:
=============
	Account: froggf@intangere.internal.intangere.com
	Subscription Id: LQBpbnRhbmdlcmVleHMuaW50YW5nZXJlLmludGVybmFsLmludGFuZ2VyZS5jb20QAAAAdyIIAinbTUGf+zpPjEyTaw==
	Prev Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHGLgAAAAAAAAE=
	More Events: false
	Status: true
	Event: StatusEvent	Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHGLgAAAAAAAAE=

INFO: <?xml version="1.0" encoding="utf-8"?><soap:Envelope ... </soap:Envelope>
INFO: Total execution time: 205

These messages will pop up every 3 minutes and indicate that the Glassfish server is receiving messages from the Exchange Server.

If you switch to Outlook and create a calendar item in Outlook, you will see a message something like the one below about 20 seconds after you create it:

INFO: Notification:
=============
	Account: froggf@intangere.internal.intangere.com
	Subscription Id: LQBpbnRhbmdlcmVleHMuaW50YW5nZXJlLmludGVybmFsLmludGFuZ2VyZS5jb20QAAAAdyIIAinbTUGf+zpPjEyTaw==
	Prev Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHGLgAAAAAAAAE=
	More Events: false
	Status: false
	Event: ModifiedEvent	Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHJLgAAAAAAAAE=
			TimeStamp: Wednesday, July 7, 2010 6:39:42 PM PDT
		Affected Item-- Type: FolderId	ID: ...	ChangeKey: AgAAAA==
	Event: ModifiedEvent	Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHKLgAAAAAAAAE=
			TimeStamp: Wednesday, July 7, 2010 6:39:42 PM PDT
		Affected Item-- Type: FolderId	ID: ...	ChangeKey: AgAAAA==
	Event: ModifiedEvent	Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHLLgAAAAAAAAE=
			TimeStamp: Wednesday, July 7, 2010 6:39:42 PM PDT
		Affected Item-- Type: FolderId	ID: ...	ChangeKey: AgAAAA==
	Event: CreatedEvent	Watermark: AQAAANnz8X+xbA5HtnQ8PXPRJfHRLgAAAAAAAAE=
			TimeStamp: Wednesday, July 7, 2010 6:39:42 PM PDT
		Affected Item-- Type: ItemId	ID: ...	ChangeKey: DwAAAA==
		Appointment-- Subject: New Appointment for Software Gorilla test
			Location: Who cares where! Just make sure it works!!
			Start: Wednesday, July 7, 2010 7:00:00 PM PDT	End: Wednesday, July 7, 2010 8:00:00 PM PDT

INFO: <?xml version="1.0" encoding="utf-8"?><soap:Envelope ... </soap:Envelope>
INFO: Total execution time: 75

I have edited this slightly so it fits on the screen. This notification informs you that the Exchange Server has called the Java HTTP Servlet which has processed the notification.

If you supplied an OpenEdge URL for the AppServer, and you connect to the database that contains the ews_update table from OpenEdge and execute the following code, you will find entries in the database for each appointment that you created:

for each ews_update:
    display skip(1)
        ews_update.cEmailAddress skip 
        ews_update.cSubject skip
        ews_update.cLocation skip
        ews_update.dTimeStamp skip
        ews_update.dStartTime ews_update.dEndTime skip(2)
      with 1 down side-labels width 100.
end.

Data from the Exchange Notification reaching OpenEdgeThere are a couple of important things to note about the data in the OpenEdge database. If you are not living in the UTC timezone, you will see a difference between the time that OpenEdge reports and the time that notification reported. That’s to be expected. Time in the database is reported as UTC. In my case, I am in the PDT (Pacific Daylight Time) timezone.

Second, it looks like the subject and location have been truncated. Again, no surprise. I set the formats in the database fields to X(50) for the subject and location, although the database will store more than that for them.

Looking back to the notification that we received from Exchange, you’ll note that there is a Watermark for each event, and that each Watermark is different (sometimes by only one character). The Watermark provides a way of going back and verifying that you have received all events from Exchange, but it is important to realize that subscribing with an old Watermark can have significant performance impacts on Exchange.

You’ll also notice that there is a Total Execution Time for each notification. This is the time taken (in milliseconds) from the time the notification was received, until the time the response was flushed out to the HTTP stream. In other words, it is the total time taken to process the request. What is interesting about this is that it takes as much as 4 times longer to process a StatusEvent than it does to process a real notification. I have determined that this has to do with the time it takes to read the original notification message off the input stream, and I have not yet figured out how to shorten that time.

Now that you have seen the notification mechanism working, let’s look at what is going on under the covers.

Receiving Notifications – EWSListener

As noted, notifications are received on the http://<server-name>:8080/EWSAPI/EWSListener URL. To achieve this, the EWSListener.java class in com.tsg.ews listens on the URL for incoming requests, so go ahead an open EWSListener.java in Eclipse.

All of the work associated with processing a request occurs in the doPost method that runs between lines 52 and line 170. I have included a lot of comments in the code that is in the zip file, so I am going to extract sections of it here and exclude the comments that are in the source. Line numbers, though, correspond with the source in the zip file.

Receiving the Notification (2.1 in the diagram above)

Lines 52 through 63 deal with receiving the notification:

protected void doPost(HttpServletRequest request, 
		HttpServletResponse response) throws ServletException, IOException {
	boolean safeDoc = false;
	boolean canceled = false;

	long startTime = GregorianCalendar.getInstance().getTimeInMillis();
	long endTime;

	try {

		String result = ExchangeService.getResult(request.getInputStream());
		Document doc = loadXML(result);
		safeDoc = true;

		Notification nfcn = null;
		try {
			nfcn = new Notification(doc);

The notification XML document is received from Exchange in the request object. The response object will contain our response back to Exchange later.

The safeDoc flag is used to indicate that we have successfully parsed the XML document and that it is indeed a document that we expect. The canceled flag indicates that the response to Exchange should be an unsubscribe message. The startTime and endTime fields are used to calculate the total elapsed time of the call.

In line 63, we make a call to getResult (now a static method) on the ExchangeService class. This method simply reads all the data off an input stream.

Now that we have a character string, it’s time to do something with it, and in line 66, we make a call to the internal loadXML method which parses the XML string into a DOM document.

In line 74, we instantiate a Notification object with the XML document and the Notification object takes care of parsing the the DOM into appropriate objects.

Parsing the XML

I’m not going to spend any time walking through the code that parses the XML document, but the following XML is from an actual notification for the creation of a new appointment (I’ve truncated IDs for brevity):

<?xml version="1.0" encoding="utf-8"?>
<soap11:Header /> 
    <soap11:Body>
        <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
                               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
            <m:ResponseMessages>
                <m:SendNotificationResponseMessage ResponseClass="Success">
                    <m:ResponseCode>NoError</m:ResponseCode> 
                        <m:Notification>
                            <t:SubscriptionId>LwBncnzAg=</t:SubscriptionId> 
                            <t:PreviousWatermark>AQAAAAE=</t:PreviousWatermark> 
                            <t:MoreEvents>false</t:MoreEvents> 
                            <t:ModifiedEvent>
                                <t:Watermark>AQAAAAAE=</t:Watermark> 
                                <t:TimeStamp>2010-07-05T18:06:29Z</t:TimeStamp> 
                                <t:FolderId Id="AQAoAnnJwAAAx4AAAA=" ChangeKey="AgAAAA==" /> 
                                <t:ParentFolderId Id="AQAoAAAxIAAAA=" ChangeKey="AQAAAA==" /> 
                            </t:ModifiedEvent>
                            <t:CreatedEvent>
                                <t:Watermark>AQAAAAE=</t:Watermark> 
                                <t:TimeStamp>2010-07-05T18:06:29Z</t:TimeStamp> 
                                <t:ItemId Id="AAAoAGF/4YLFkwAAAAQJAgAA" ChangeKey="DwAAAA==" /> 
                                <t:ParentFolderId Id="AQAoAAAx4AAAA=" ChangeKey="AQAAAA==" /> 
                            </t:CreatedEvent>
                        </m:Notification>
                    </m:SendNotificationResponseMessage>
            </m:ResponseMessages>
        </m:SendNotification>
    </soap11:Body>
</soap11:Envelope>

The Notification element contains a SubscriptionId, PreviousWatermark and MoreEvents element. When the code parses this XML document, it creates a Notification object to contain this information.

The Notification element also contains an Event element for each event for which you are receiving a notification. In this case, the first Event element is a ModifiedEvent and it applies to a Folder (in this case, the calendar folder that has been modified by the addition of a new item). The second event, the CreatedEvent element, contains an ItemId element that identifies the calendar item that was created.

The Notification is an iterable collection of SubscriptionEvent objects, so when the code parses these elements, it creates and stores a SubscriptionEvent object to hold each of these Event elements. Each Event node contains a Watermark that is applicable for the event that took place and a TimeStamp – the time that the event took place. These values are recorded against the SubscriptionEvent object.

An Event element will contain either a FolderId element or an ItemId element which is the object that was affected by the event. This element is parsed and an IDHolder object is created to contain it and it is stored as the AffectedItem property of a SubscriptionEvent object.

There is also a ParentFolderId which is the ID of the folder that contains the object that is affected by the event. When this element is parsed, its data is also stored in an IDHolder object in the ParentFolder property of the SubscriptionEvent object.

In the case of the above notification, one Notification object was created and it contained two SubscriptionEvent objects – one for the ModifiedEvent and one for the CreatedEvent.

Marrying the Notification and the Subscription

Now that we have parsed the notification, we’ll go back to the code in line 77 through 86 and deal with marrying the Notification with a Subscription object:

	Subscription sub = SubscriptionManager.getInstance().getSubscription(nfcn.getSubscriptionId());
	if (sub != null) {
		nfcn.setAccount(sub.getAccount());

		canceled = sub.isCanceled();
		if (canceled)
			SubscriptionManager.getInstance().clearSubscription(sub);

		if (!nfcn.isStatus()) {
			getAppointments(sub.getService(),nfcn);
		}

In line 77 we ask the SubscriptionManager for the Subscription object associated with the SubscriptionId in the Notification object. The next set of code only executes if we found a Subscription object.

In line 80, we set the account (e-mail address) for the notification to the account of the subscription and in line 84, we check to see if the subscription has been canceled by a call to the Unsubscribe operation. If it has, we set the canceled flag and remove the subscription from the SubscriptionManager because this is the last notification we will receive for this subscription.

As I mentioned earlier, Exchange will send notifications on a regular basis (as defined by StatusFrequency). These notifications do not contain any data; they are simply status messages (think of them as pings). When we parse the SOAP message into the Notification object, if the notification is a StatusEvent, we set a flag to indicate that this is only a status message. In line 91 we check to see if this is a status message, and if not, we make a call to getAppointments which is responsible for retrieving appointments from the Exchange Server.

getAppointments (2.3 in the diagram above)

The getAppointments method is in lines 176 through 209:

private void getAppointments(ExchangeService service, Notification nfcn) {

	for (SubscriptionEvent se: nfcn) {

		if (se.getEventType() != EventType.STATUS 
				&& se.getEventType() != EventType.DELETED 
				&& se.getAffectedItem() != null 
				&& se.getAffectedItem().getIdType() == IDType.ITEM) {

			Appointment appt = new Appointment(service);
			appt.setEmailAddress(nfcn.getAccount());

			EWSResponse resp = appt.retrieve(se.getAffectedItem().getId(),
                                                  se.getAffectedItem().getChangeKey());
			if (!resp.hasError()) {
				se.setAppointment(appt);
			}
			else {
				if (resp.getResponseCode().equals("ErrorItemNotFound")) {
					se.setEventType(EventType.DELETED);
				}
				else {
					System.out.println(...);
				}
			}
		}
	}
}

As you will recall, the notification that we receive from Exchange only contains the ID of the item that has changed. It does not provide any information about the item itself. In getAppointments we take care of retrieving those items.

Notification objects are iterable collections of SubscriptionEvents so we loop through all the SubscriptionEvents (line 179) in the Notification collection, ignoring anything that is not a Created- or ModifiedEvent for an ItemId (lines 183 to 186).

We then instantiate an Appointment object (line 189), set the account for it (line 190), and attempt to retrieve it from the Exchange Server (line 193).

If we’re successful retrieving the appointment, we set the Appointment property of the SubscriptionEvent from the appointment we retrieved (line196).

If we failed with an ErrorItemNotFound message from Exchange, it means that the item has been deleted so we cannot find any information about it. The event type therefore needs to be changed to a DeletedEvent (lines 200 through 202). This may seem strange, but remember that Exchange will move a deleted item to the Deleted Items folder which means we will receive a ModifiedEvent, rather than a DeletedEvent.

Calling OpenEdge (2.4 in the diagram above)

Returning to the code in doPost, lines 96 through 103 deal with the call to OpenEdge:

	System.out.println(nfcn.print());

	// **OESPECIFIC
	// The following code is inactive in the Java zip file version of the code.
	// If the subscription has an OpenEdge AppServer URL in it, let's call OpenEdge.
	if (sub.getOpenedgeURL().startsWith("AppServer://")) {
		OpenEdgeCallFactory.callOpenEdge(sub, nfcn);
	}

Line 96 just displays the notification information in human readable form – it is responsible for the Notification messages in the server log.

Line 101 through 103 checks if the OpenEdge URL for the subscription starts with the string “AppServer://” and, if so, calls the callOpenEdge method which is in lines 68 through 99 of the OpenEdgeCallFactory class:

public static void callOpenEdge(Subscription sub, Notification nfcn) {
	checkInitialized();
	for (SubscriptionEvent se: nfcn) {
		if (se.getAffectedItem() != null && se.getAffectedItem().getIdType() == IDType.ITEM) {
			Call call = new Call(OECALL);
			call.setParameterValue(PAR_EMAIL, sub.getAccount());
			call.setParameterValue(PAR_SUBSCRIPTION_ID, nfcn.getSubscriptionId());
			call.setParameterValue(PAR_EVENT_TYPE, se.getEventType().toString());
			call.setParameterValue(PAR_TIME_STAMP, se.getTimeStamp());
			call.setParameterValue(PAR_WATERMARK, se.getWatermark());
			call.setParameterValue(PAR_ITEM_ID, se.getAffectedItem().getId());
			call.setParameterValue(PAR_CHANGE_KEY, se.getAffectedItem().getChangeKey());
			Appointment appt = se.getAppointment();
			if (appt != null) {
				call.setParameterValue(PAR_SUBJECT, appt.getDescription());
				call.setParameterValue(PAR_LOCATION, appt.getLocation());
				call.setParameterValue(PAR_NOTES, appt.getNotes());
				call.setParameterValue(PAR_START_TIME, appt.getStartDate());
				call.setParameterValue(PAR_END_TIME, appt.getEndDate());
				call.setParameterValue(PAR_REMINDER_MINUTES, appt.getReminderMinutes());
				call.setParameterValue(PAR_REMINDER_SET, appt.isReminderSet());
			}
			call.executeCall(sub.getOpenedgeURL());
			if (call.isReturnError()) {
				System.out.println(...);
			}
			else if (call.getFailure() != null) {
				System.out.println(...);
			}
		}
	}	
}

This method simply instantiates a Call object (line 72), sets the parameters for the call to ewscall.p (lines 73 through 89), and executes the call using the OpenEdge URL (line 90). A successful call to ewscall.p will result in the data being written to the OpenEdge database. If you read my article on the OpenEdge Dynamic OpenClient Java Example, this code will be reasonably familiar.

At this point, all that is left to do is update the subscription and respond to the Exchange Server.

Updating the Subscription

From the information in David Sterling, et al’s book, Inside Microsoft Exchange Server 2007 Web Services, I had understood that the Watermark was associated with a subscription, but in working with this a little more, I have come to the conclusion that the Watermark is associated with the Exchange Server as a whole. I had been hoping to use the Watermark to guarantee that I knew that I had received all events from the Exchange Server, but it does not seem to work the way I had understood. I’m still piecing this together at the time of writing, so the code that is included with the example is predicated on my understanding from Sterling, et al.

Lines 107 through 127 deal with updating the subscription:

			if (!sub.getWatermark().equals(nfcn.getPreviousWatermark())) {
				System.out.println(...);
			}

			String lastWm = null;
			for (SubscriptionEvent se : nfcn) {
				lastWm = se.getWatermark();
			}

			if (lastWm != null) {
				sub.setWatermark(lastWm);
			}
		}
		else {
			canceled = true;
			System.out.println(...);
		}
	}

In line 107 through 109, we check to see if the subscription’s current watermark matches the previous watermark of the notification we received. If it doesn’t (and if my understanding of Sterling, et al is correct, it should), we print a warning message to the log.

In lines 112 through 116, we loop through all the SubscriptionEvent objects and store the last watermark and line 117 through 119, we set the Watermark on the Subscription to the Watermark of the last SubscriptionEvent.

In lines 121 through 125 we deal with the situation where we were unable to match the notification with a subscription – we set the canceled flag to true so that the subscription will terminate.

Responding to Exchange (2.2 in the diagram above)

In lines 138 through 170 we deal with responding to Exchange:

	if (!safeDoc) {
		response.sendError(HttpServletResponse.SC_BAD_REQUEST);
	}
	else {  
		String str;

		if (canceled) {
			str = getResponseXML(UNSUBSCRIBE);
		}
		else {  
			str = getResponseXML(OK);
		}

		response.setCharacterEncoding("UTF-8");
		response.setStatus(HttpServletResponse.SC_OK);
		response.setContentType("text/xml; charset=UTF-8");
		response.setContentLength(str.length());

		PrintWriter w = response.getWriter();
		w.print(str);
		w.flush();
	}
	response.flushBuffer();
	endTime = GregorianCalendar.getInstance().getTimeInMillis();
	System.out.println(String.format("Total execution time: %1$s", (Long) (endTime - startTime)));
}

In lines 139 through 141, we handle the situation where we received a bad message from Exchange – we return an HTTP 400 error.

In lines 143 through 151 we check to see if the canceled flag is set. If it is, we build an Unsubscribe response, otherwise we just build an OK acknowledgement response. Lines 154 through 157 set some properties of the response object and lines 160 through 167 write the response to the output stream and flush the buffers.

Finally, in lines 168 and 169 we determine the amount of time that has elapsed since the notification was received and we display this.

Summary

That’s it. Now you have seen how to handle the Exchange Web Services Push Subscription API in Java. Although this may seem like a convoluted piece of code, I have found it to be extraordinarily stable. I have experimented with taking down the Glassfish server and bringing it back up and the notifications keep on coming.

I have left the server running for days with no updates happening on the Exchange Server with no problem at all. I have also exposed it to some pretty serious stress and it has performed with no problems at all on fairly small hardware and software configurations.

Most of the time, notifications, including the call back to Exchange Server and OpenEdge, are being processed in around 100ms. StatusEvents take just over 200ms.

The one thing I am still having trouble with is trying to decide on an appropriate StatusFrequency. I would like to set the StatusFrequency to 10 minutes. That way, it will take 30 minutes for a subscription to expire. The problem is that clean cancellations rely on regular status events, so anything longer than 3 minutes can seem like an eternity. One thing I have definitely concluded is that the StatusFrequency needs to be configurable.

I hope this article has been useful. The next article in this series is going to deal with determining availability, but I am going to be engaged in some pretty serious development work for the next few weeks so it is likely that the next article in this series will only see the light of day toward the end of the Summer.

Let me have your feedback…

GD Star Rating
loading...