001 /*
002 * The MIT License
003 * Copyright (c) 2012 Microsoft Corporation
004 *
005 * Permission is hereby granted, free of charge, to any person obtaining a copy
006 * of this software and associated documentation files (the "Software"), to deal
007 * in the Software without restriction, including without limitation the rights
008 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
009 * copies of the Software, and to permit persons to whom the Software is
010 * furnished to do so, subject to the following conditions:
011 *
012 * The above copyright notice and this permission notice shall be included in
013 * all copies or substantial portions of the Software.
014 *
015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
016 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
017 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
018 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
019 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
020 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
021 * THE SOFTWARE.
022 */
023
024 package microsoft.exchange.webservices.data.notification;
025
026 import microsoft.exchange.webservices.data.core.EwsUtilities;
027 import microsoft.exchange.webservices.data.core.ExchangeService;
028 import microsoft.exchange.webservices.data.core.exception.misc.ArgumentNullException;
029 import microsoft.exchange.webservices.data.core.request.GetStreamingEventsRequest;
030 import microsoft.exchange.webservices.data.core.request.HangingRequestDisconnectEventArgs;
031 import microsoft.exchange.webservices.data.core.request.HangingServiceRequestBase;
032 import microsoft.exchange.webservices.data.core.response.GetStreamingEventsResponse;
033 import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
034 import microsoft.exchange.webservices.data.core.enumeration.misc.error.ServiceError;
035 import microsoft.exchange.webservices.data.core.enumeration.service.ServiceResult;
036 import microsoft.exchange.webservices.data.core.exception.misc.ArgumentException;
037 import microsoft.exchange.webservices.data.core.exception.misc.ArgumentOutOfRangeException;
038 import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
039 import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceResponseException;
040 import org.apache.commons.logging.Log;
041 import org.apache.commons.logging.LogFactory;
042
043 import java.io.Closeable;
044 import java.util.ArrayList;
045 import java.util.HashMap;
046 import java.util.List;
047 import java.util.Map;
048
049 /**
050 * Represents a connection to an ongoing stream of events.
051 */
052 public final class StreamingSubscriptionConnection implements Closeable,
053 HangingServiceRequestBase.IHandleResponseObject,
054 HangingServiceRequestBase.IHangingRequestDisconnectHandler {
055
056 private static final Log LOG = LogFactory.getLog(StreamingSubscriptionConnection.class);
057
058 /**
059 * Mapping of streaming id to subscriptions currently on the connection.
060 */
061 private Map<String, StreamingSubscription> subscriptions;
062
063 /**
064 * connection lifetime, in minutes
065 */
066 private int connectionTimeout;
067
068 /**
069 * ExchangeService instance used to make the EWS call.
070 */
071 private ExchangeService session;
072
073 /**
074 * Value indicating whether the class is disposed.
075 */
076 private boolean isDisposed;
077
078 /**
079 * Currently used instance of a GetStreamingEventsRequest connected to EWS.
080 */
081 private GetStreamingEventsRequest currentHangingRequest;
082
083
084 public interface INotificationEventDelegate {
085 /**
086 * Represents a delegate that is invoked when notification are received
087 * from the server
088 *
089 * @param sender The StreamingSubscriptionConnection instance that received
090 * the events.
091 * @param args The event data.
092 */
093 void notificationEventDelegate(Object sender, NotificationEventArgs args);
094 }
095
096
097 /**
098 * Notification events Occurs when notification are received from the
099 * server.
100 */
101 private List<INotificationEventDelegate> onNotificationEvent = new ArrayList<INotificationEventDelegate>();
102
103 /**
104 * Set event to happen when property Notify.
105 *
106 * @param notificationEvent notification event
107 */
108 public void addOnNotificationEvent(
109 INotificationEventDelegate notificationEvent) {
110 onNotificationEvent.add(notificationEvent);
111 }
112
113 /**
114 * Remove the event from happening when property Notify.
115 *
116 * @param notificationEvent notification event
117 */
118 public void removeNotificationEvent(
119 INotificationEventDelegate notificationEvent) {
120 onNotificationEvent.remove(notificationEvent);
121 }
122
123 /**
124 * Clears notification events list.
125 */
126 public void clearNotificationEvent() {
127 onNotificationEvent.clear();
128 }
129
130 public interface ISubscriptionErrorDelegate {
131
132 /**
133 * Represents a delegate that is invoked when an error occurs within a
134 * streaming subscription connection.
135 *
136 * @param sender The StreamingSubscriptionConnection instance within which
137 * the error occurred.
138 * @param args The event data.
139 */
140 void subscriptionErrorDelegate(Object sender,
141 SubscriptionErrorEventArgs args);
142 }
143
144
145 /**
146 * Subscription events Occur when a subscription encounters an error.
147 */
148 private List<ISubscriptionErrorDelegate> onSubscriptionError = new ArrayList<ISubscriptionErrorDelegate>();
149
150 /**
151 * Set event to happen when property subscriptionError.
152 *
153 * @param subscriptionError subscription event
154 */
155 public void addOnSubscriptionError(
156 ISubscriptionErrorDelegate subscriptionError) {
157 onSubscriptionError.add(subscriptionError);
158 }
159
160 /**
161 * Remove the event from happening when property subscription.
162 *
163 * @param subscriptionError subscription event
164 */
165 public void removeSubscriptionError(
166 ISubscriptionErrorDelegate subscriptionError) {
167 onSubscriptionError.remove(subscriptionError);
168 }
169
170 /**
171 * Clears subscription events list.
172 */
173 public void clearSubscriptionError() {
174 onSubscriptionError.clear();
175 }
176
177 /**
178 * Disconnect events Occurs when a streaming subscription connection is
179 * disconnected from the server.
180 */
181 private List<ISubscriptionErrorDelegate> onDisconnect = new ArrayList<ISubscriptionErrorDelegate>();
182
183 /**
184 * Set event to happen when property disconnect.
185 *
186 * @param disconnect disconnect event
187 */
188 public void addOnDisconnect(ISubscriptionErrorDelegate disconnect) {
189 onDisconnect.add(disconnect);
190 }
191
192 /**
193 * Remove the event from happening when property disconnect.
194 *
195 * @param disconnect disconnect event
196 */
197 public void removeDisconnect(ISubscriptionErrorDelegate disconnect) {
198 onDisconnect.remove(disconnect);
199 }
200
201 /**
202 * Clears disconnect events list.
203 */
204 public void clearDisconnect() {
205 onDisconnect.clear();
206 }
207
208 /**
209 * Initializes a new instance of the StreamingSubscriptionConnection class.
210 *
211 * @param service The ExchangeService instance this connection uses to connect
212 * to the server.
213 * @param lifetime The maximum time, in minutes, the connection will remain open.
214 * Lifetime must be between 1 and 30.
215 * @throws Exception
216 */
217 public StreamingSubscriptionConnection(ExchangeService service, int lifetime)
218 throws Exception {
219 EwsUtilities.validateParam(service, "service");
220
221 EwsUtilities.validateClassVersion(service,
222 ExchangeVersion.Exchange2010_SP1, this.getClass().getName());
223
224 if (lifetime < 1 || lifetime > 30) {
225 throw new ArgumentOutOfRangeException("lifetime");
226 }
227
228 this.session = service;
229 this.subscriptions = new HashMap<String, StreamingSubscription>();
230 this.connectionTimeout = lifetime;
231 }
232
233 /**
234 * Initializes a new instance of the StreamingSubscriptionConnection class.
235 *
236 * @param service The ExchangeService instance this connection uses to connect
237 * to the server.
238 * @param subscriptions Iterable subcriptions
239 * @param lifetime The maximum time, in minutes, the connection will remain open.
240 * Lifetime must be between 1 and 30.
241 * @throws Exception
242 */
243 public StreamingSubscriptionConnection(ExchangeService service,
244 Iterable<StreamingSubscription> subscriptions, int lifetime)
245 throws Exception {
246 this(service, lifetime);
247 EwsUtilities.validateParamCollection(subscriptions.iterator(), "subscriptions");
248 for (StreamingSubscription subscription : subscriptions) {
249 this.subscriptions.put(subscription.getId(), subscription);
250 }
251 }
252
253 /**
254 * Adds a subscription to this connection.
255 *
256 * @param subscription The subscription to add.
257 * @throws Exception Thrown when AddSubscription is called while connected.
258 */
259 public void addSubscription(StreamingSubscription subscription)
260 throws Exception {
261 this.throwIfDisposed();
262 EwsUtilities.validateParam(subscription, "subscription");
263 this.validateConnectionState(false, "Subscriptions can't be added to an open connection.");
264
265 synchronized (this) {
266 if (this.subscriptions.containsKey(subscription.getId())) {
267 return;
268 }
269 this.subscriptions.put(subscription.getId(), subscription);
270 }
271 }
272
273 /**
274 * Removes the specified streaming subscription from the connection.
275 *
276 * @param subscription The subscription to remove.
277 * @throws Exception Thrown when RemoveSubscription is called while connected.
278 */
279 public void removeSubscription(StreamingSubscription subscription)
280 throws Exception {
281 this.throwIfDisposed();
282
283 EwsUtilities.validateParam(subscription, "subscription");
284
285 this.validateConnectionState(false, "Subscriptions can't be removed from an open connection.");
286
287 synchronized (this) {
288 this.subscriptions.remove(subscription.getId());
289 }
290 }
291
292 /**
293 * Opens this connection so it starts receiving events from the server.This
294 * results in a long-standing call to EWS.
295 *
296 * @throws Exception
297 * @throws ServiceLocalException Thrown when Open is called while connected.
298 */
299 public void open() throws ServiceLocalException, Exception {
300 synchronized (this) {
301 this.throwIfDisposed();
302
303 this.validateConnectionState(false, "The connection has already opened.");
304
305 if (this.subscriptions.size() == 0) {
306 throw new ServiceLocalException(
307 "You must add at least one subscription to this connection before it can be opened.");
308 }
309
310 this.currentHangingRequest = new GetStreamingEventsRequest(
311 this.session, this, this.subscriptions.keySet(),
312 this.connectionTimeout);
313
314 this.currentHangingRequest.addOnDisconnectEvent(this);
315
316 this.currentHangingRequest.internalExecute();
317 }
318 }
319
320 /**
321 * Called when the request is disconnected.
322 *
323 * @param sender The sender.
324 * @param args The Microsoft.Exchange.WebServices.Data.
325 * HangingRequestDisconnectEventArgs instance containing the
326 * event data.
327 */
328 private void onRequestDisconnect(Object sender,
329 HangingRequestDisconnectEventArgs args) {
330 this.internalOnDisconnect(args.getException());
331 }
332
333 /**
334 * Closes this connection so it stops receiving events from the server.This
335 * terminates a long-standing call to EWS.
336 */
337 public void close() {
338 synchronized (this) {
339 try {
340 this.throwIfDisposed();
341
342 this.validateConnectionState(true, "The connection is already closed.");
343
344 // Further down in the stack, this will result in a
345 // call to our OnRequestDisconnect event handler,
346 // doing the necessary cleanup.
347 this.currentHangingRequest.disconnect();
348 } catch (Exception e) {
349 LOG.error(e);
350 }
351 }
352 }
353
354 /**
355 * Internal helper method called when the request disconnects.
356 *
357 * @param ex The exception that caused the disconnection. May be null.
358 */
359 private void internalOnDisconnect(Exception ex) {
360 if (!onDisconnect.isEmpty()) {
361 for (ISubscriptionErrorDelegate disconnect : onDisconnect) {
362 disconnect.subscriptionErrorDelegate(this,
363 new SubscriptionErrorEventArgs(null, ex));
364 }
365 }
366 this.currentHangingRequest = null;
367 }
368
369 /**
370 * Gets a value indicating whether this connection is opened
371 *
372 * @throws Exception
373 */
374 public boolean getIsOpen() throws Exception {
375
376 this.throwIfDisposed();
377 if (this.currentHangingRequest == null) {
378 return false;
379 } else {
380 return this.currentHangingRequest.isConnected();
381 }
382
383 }
384
385 /**
386 * Validates the state of the connection.
387 *
388 * @param isConnectedExpected Value indicating whether we expect to be currently connected.
389 * @param errorMessage The error message.
390 * @throws Exception
391 */
392 private void validateConnectionState(boolean isConnectedExpected,
393 String errorMessage) throws Exception {
394 if ((isConnectedExpected && !this.getIsOpen())
395 || (!isConnectedExpected && this.getIsOpen())) {
396 throw new ServiceLocalException(errorMessage);
397 }
398 }
399
400 /**
401 * Handles the service response object.
402 *
403 * @param response The response.
404 * @throws ArgumentException
405 */
406 private void handleServiceResponseObject(Object response)
407 throws ArgumentException {
408 GetStreamingEventsResponse gseResponse = (GetStreamingEventsResponse) response;
409
410 if (gseResponse == null) {
411 throw new ArgumentNullException("GetStreamingEventsResponse must not be null",
412 "GetStreamingEventsResponse");
413 } else {
414 if (gseResponse.getResult() == ServiceResult.Success
415 || gseResponse.getResult() == ServiceResult.Warning) {
416 if (gseResponse.getResults().getNotifications().size() > 0) {
417 // We got notification; dole them out.
418 this.issueNotificationEvents(gseResponse);
419 } else {
420 // // This was just a heartbeat, nothing to do here.
421 }
422 } else if (gseResponse.getResult() == ServiceResult.Error) {
423 if (gseResponse.getErrorSubscriptionIds() == null
424 || gseResponse.getErrorSubscriptionIds().size() == 0) {
425 // General error
426 this.issueGeneralFailure(gseResponse);
427 } else {
428 // subscription-specific errors
429 this.issueSubscriptionFailures(gseResponse);
430 }
431 }
432 }
433 }
434
435 /**
436 * Issues the subscription failures.
437 *
438 * @param gseResponse The GetStreamingEvents response.
439 */
440 private void issueSubscriptionFailures(
441 GetStreamingEventsResponse gseResponse) {
442 ServiceResponseException exception = new ServiceResponseException(
443 gseResponse);
444
445 for (String id : gseResponse.getErrorSubscriptionIds()) {
446 StreamingSubscription subscription = null;
447
448 synchronized (this) {
449 // Client can do any good or bad things in the below event
450 // handler
451 if (this.subscriptions != null
452 && this.subscriptions.containsKey(id)) {
453 subscription = this.subscriptions.get(id);
454 }
455
456 }
457 if (subscription != null) {
458 SubscriptionErrorEventArgs eventArgs = new SubscriptionErrorEventArgs(
459 subscription, exception);
460
461 if (!onSubscriptionError.isEmpty()) {
462 for (ISubscriptionErrorDelegate subError : onSubscriptionError) {
463 subError.subscriptionErrorDelegate(this, eventArgs);
464 }
465 }
466 }
467 if (gseResponse.getErrorCode() != ServiceError.ErrorMissedNotificationEvents) {
468 // Client can do any good or bad things in the above event
469 // handler
470 synchronized (this) {
471 if (this.subscriptions != null
472 && this.subscriptions.containsKey(id)) {
473 // We are no longer servicing the subscription.
474 this.subscriptions.remove(id);
475 }
476 }
477 }
478 }
479 }
480
481 /**
482 * Issues the general failure.
483 *
484 * @param gseResponse The GetStreamingEvents response.
485 */
486 private void issueGeneralFailure(GetStreamingEventsResponse gseResponse) {
487 SubscriptionErrorEventArgs eventArgs = new SubscriptionErrorEventArgs(
488 null, new ServiceResponseException(gseResponse));
489
490 if (!onSubscriptionError.isEmpty()) {
491 for (ISubscriptionErrorDelegate subError : onSubscriptionError) {
492 subError.subscriptionErrorDelegate(this, eventArgs);
493 }
494 }
495 }
496
497 /**
498 * Issues the notification events.
499 *
500 * @param gseResponse The GetStreamingEvents response.
501 */
502 private void issueNotificationEvents(GetStreamingEventsResponse gseResponse) {
503
504 for (GetStreamingEventsResults.NotificationGroup events : gseResponse
505 .getResults().getNotifications()) {
506 StreamingSubscription subscription = null;
507
508 synchronized (this) {
509 // Client can do any good or bad things in the below event
510 // handler
511 if (this.subscriptions != null
512 && this.subscriptions
513 .containsKey(events.subscriptionId)) {
514 subscription = this.subscriptions
515 .get(events.subscriptionId);
516 }
517 }
518 if (subscription != null) {
519 NotificationEventArgs eventArgs = new NotificationEventArgs(
520 subscription, events.events);
521
522 if (!onNotificationEvent.isEmpty()) {
523 for (INotificationEventDelegate notifyEvent : onNotificationEvent) {
524 notifyEvent.notificationEventDelegate(this, eventArgs);
525 }
526 }
527 }
528 }
529 }
530
531 /**
532 * Frees resources associated with this StreamingSubscriptionConnection.
533 */
534 public void dispose() {
535 synchronized (this) {
536 if (!this.isDisposed) {
537 if (this.currentHangingRequest != null) {
538 this.currentHangingRequest = null;
539 }
540
541 this.subscriptions = null;
542 this.session = null;
543
544 this.isDisposed = true;
545 }
546 }
547 }
548
549 /**
550 * Throws if disposed.
551 *
552 * @throws Exception
553 */
554 private void throwIfDisposed() throws Exception {
555 if (this.isDisposed) {
556 throw new Exception(this.getClass().getName());
557 }
558 }
559
560 @Override
561 public void handleResponseObject(Object response) throws ArgumentException {
562 this.handleServiceResponseObject(response);
563 }
564
565 @Override
566 public void hangingRequestDisconnectHandler(Object sender,
567 HangingRequestDisconnectEventArgs args) {
568 this.onRequestDisconnect(sender, args);
569 }
570
571 }