microsoft.exchange.webservices.data.notification.StreamingSubscriptionConnection.java Source code

Java tutorial

Introduction

Here is the source code for microsoft.exchange.webservices.data.notification.StreamingSubscriptionConnection.java

Source

/*
 * The MIT License
 * Copyright (c) 2012 Microsoft Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package microsoft.exchange.webservices.data.notification;

import microsoft.exchange.webservices.data.core.EwsUtilities;
import microsoft.exchange.webservices.data.core.ExchangeService;
import microsoft.exchange.webservices.data.core.exception.misc.ArgumentNullException;
import microsoft.exchange.webservices.data.core.request.GetStreamingEventsRequest;
import microsoft.exchange.webservices.data.core.request.HangingRequestDisconnectEventArgs;
import microsoft.exchange.webservices.data.core.request.HangingServiceRequestBase;
import microsoft.exchange.webservices.data.core.response.GetStreamingEventsResponse;
import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
import microsoft.exchange.webservices.data.core.enumeration.misc.error.ServiceError;
import microsoft.exchange.webservices.data.core.enumeration.service.ServiceResult;
import microsoft.exchange.webservices.data.core.exception.misc.ArgumentException;
import microsoft.exchange.webservices.data.core.exception.misc.ArgumentOutOfRangeException;
import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceResponseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Represents a connection to an ongoing stream of events.
 */
public final class StreamingSubscriptionConnection
        implements Closeable, HangingServiceRequestBase.IHandleResponseObject,
        HangingServiceRequestBase.IHangingRequestDisconnectHandler {

    private static final Log LOG = LogFactory.getLog(StreamingSubscriptionConnection.class);

    /**
     * Mapping of streaming id to subscriptions currently on the connection.
     */
    private Map<String, StreamingSubscription> subscriptions;

    /**
     * connection lifetime, in minutes
     */
    private int connectionTimeout;

    /**
     * ExchangeService instance used to make the EWS call.
     */
    private ExchangeService session;

    /**
     * Value indicating whether the class is disposed.
     */
    private boolean isDisposed;

    /**
     * Currently used instance of a GetStreamingEventsRequest connected to EWS.
     */
    private GetStreamingEventsRequest currentHangingRequest;

    public interface INotificationEventDelegate {
        /**
         * Represents a delegate that is invoked when notification are received
         * from the server
         *
         * @param sender The StreamingSubscriptionConnection instance that received
         *               the events.
         * @param args   The event data.
         */
        void notificationEventDelegate(Object sender, NotificationEventArgs args);
    }

    /**
     * Notification events Occurs when notification are received from the
     * server.
     */
    private List<INotificationEventDelegate> onNotificationEvent = new ArrayList<INotificationEventDelegate>();

    /**
     * Set event to happen when property Notify.
     *
     * @param notificationEvent notification event
     */
    public void addOnNotificationEvent(INotificationEventDelegate notificationEvent) {
        onNotificationEvent.add(notificationEvent);
    }

    /**
     * Remove the event from happening when property Notify.
     *
     * @param notificationEvent notification event
     */
    public void removeNotificationEvent(INotificationEventDelegate notificationEvent) {
        onNotificationEvent.remove(notificationEvent);
    }

    /**
     * Clears notification events list.
     */
    public void clearNotificationEvent() {
        onNotificationEvent.clear();
    }

    public interface ISubscriptionErrorDelegate {

        /**
         * Represents a delegate that is invoked when an error occurs within a
         * streaming subscription connection.
         *
         * @param sender The StreamingSubscriptionConnection instance within which
         *               the error occurred.
         * @param args   The event data.
         */
        void subscriptionErrorDelegate(Object sender, SubscriptionErrorEventArgs args);
    }

    /**
     * Subscription events Occur when a subscription encounters an error.
     */
    private List<ISubscriptionErrorDelegate> onSubscriptionError = new ArrayList<ISubscriptionErrorDelegate>();

    /**
     * Set event to happen when property subscriptionError.
     *
     * @param subscriptionError subscription event
     */
    public void addOnSubscriptionError(ISubscriptionErrorDelegate subscriptionError) {
        onSubscriptionError.add(subscriptionError);
    }

    /**
     * Remove the event from happening when property subscription.
     *
     * @param subscriptionError subscription event
     */
    public void removeSubscriptionError(ISubscriptionErrorDelegate subscriptionError) {
        onSubscriptionError.remove(subscriptionError);
    }

    /**
     * Clears subscription events list.
     */
    public void clearSubscriptionError() {
        onSubscriptionError.clear();
    }

    /**
     * Disconnect events Occurs when a streaming subscription connection is
     * disconnected from the server.
     */
    private List<ISubscriptionErrorDelegate> onDisconnect = new ArrayList<ISubscriptionErrorDelegate>();

    /**
     * Set event to happen when property disconnect.
     *
     * @param disconnect disconnect event
     */
    public void addOnDisconnect(ISubscriptionErrorDelegate disconnect) {
        onDisconnect.add(disconnect);
    }

    /**
     * Remove the event from happening when property disconnect.
     *
     * @param disconnect disconnect event
     */
    public void removeDisconnect(ISubscriptionErrorDelegate disconnect) {
        onDisconnect.remove(disconnect);
    }

    /**
     * Clears disconnect events list.
     */
    public void clearDisconnect() {
        onDisconnect.clear();
    }

    /**
     * Initializes a new instance of the StreamingSubscriptionConnection class.
     *
     * @param service  The ExchangeService instance this connection uses to connect
     *                 to the server.
     * @param lifetime The maximum time, in minutes, the connection will remain open.
     *                 Lifetime must be between 1 and 30.
     * @throws Exception
     */
    public StreamingSubscriptionConnection(ExchangeService service, int lifetime) throws Exception {
        EwsUtilities.validateParam(service, "service");

        EwsUtilities.validateClassVersion(service, ExchangeVersion.Exchange2010_SP1, this.getClass().getName());

        if (lifetime < 1 || lifetime > 30) {
            throw new ArgumentOutOfRangeException("lifetime");
        }

        this.session = service;
        this.subscriptions = new HashMap<String, StreamingSubscription>();
        this.connectionTimeout = lifetime;
    }

    /**
     * Initializes a new instance of the StreamingSubscriptionConnection class.
     *
     * @param service       The ExchangeService instance this connection uses to connect
     *                      to the server.
     * @param subscriptions Iterable subcriptions
     * @param lifetime      The maximum time, in minutes, the connection will remain open.
     *                      Lifetime must be between 1 and 30.
     * @throws Exception
     */
    public StreamingSubscriptionConnection(ExchangeService service, Iterable<StreamingSubscription> subscriptions,
            int lifetime) throws Exception {
        this(service, lifetime);
        EwsUtilities.validateParamCollection(subscriptions.iterator(), "subscriptions");
        for (StreamingSubscription subscription : subscriptions) {
            this.subscriptions.put(subscription.getId(), subscription);
        }
    }

    /**
     * Adds a subscription to this connection.
     *
     * @param subscription The subscription to add.
     * @throws Exception Thrown when AddSubscription is called while connected.
     */
    public void addSubscription(StreamingSubscription subscription) throws Exception {
        this.throwIfDisposed();
        EwsUtilities.validateParam(subscription, "subscription");
        this.validateConnectionState(false, "Subscriptions can't be added to an open connection.");

        synchronized (this) {
            if (this.subscriptions.containsKey(subscription.getId())) {
                return;
            }
            this.subscriptions.put(subscription.getId(), subscription);
        }
    }

    /**
     * Removes the specified streaming subscription from the connection.
     *
     * @param subscription The subscription to remove.
     * @throws Exception Thrown when RemoveSubscription is called while connected.
     */
    public void removeSubscription(StreamingSubscription subscription) throws Exception {
        this.throwIfDisposed();

        EwsUtilities.validateParam(subscription, "subscription");

        this.validateConnectionState(false, "Subscriptions can't be removed from an open connection.");

        synchronized (this) {
            this.subscriptions.remove(subscription.getId());
        }
    }

    /**
     * Opens this connection so it starts receiving events from the server.This
     * results in a long-standing call to EWS.
     *
     * @throws Exception
     * @throws ServiceLocalException Thrown when Open is called while connected.
     */
    public void open() throws ServiceLocalException, Exception {
        synchronized (this) {
            this.throwIfDisposed();

            this.validateConnectionState(false, "The connection has already opened.");

            if (this.subscriptions.size() == 0) {
                throw new ServiceLocalException(
                        "You must add at least one subscription to this connection before it can be opened.");
            }

            this.currentHangingRequest = new GetStreamingEventsRequest(this.session, this,
                    this.subscriptions.keySet(), this.connectionTimeout);

            this.currentHangingRequest.addOnDisconnectEvent(this);

            this.currentHangingRequest.internalExecute();
        }
    }

    /**
     * Called when the request is disconnected.
     *
     * @param sender The sender.
     * @param args   The Microsoft.Exchange.WebServices.Data.
     *               HangingRequestDisconnectEventArgs instance containing the
     *               event data.
     */
    private void onRequestDisconnect(Object sender, HangingRequestDisconnectEventArgs args) {
        this.internalOnDisconnect(args.getException());
    }

    /**
     * Closes this connection so it stops receiving events from the server.This
     * terminates a long-standing call to EWS.
     */
    public void close() {
        synchronized (this) {
            try {
                this.throwIfDisposed();

                this.validateConnectionState(true, "The connection is already closed.");

                // Further down in the stack, this will result in a
                // call to our OnRequestDisconnect event handler,
                // doing the necessary cleanup.
                this.currentHangingRequest.disconnect();
            } catch (Exception e) {
                LOG.error(e);
            }
        }
    }

    /**
     * Internal helper method called when the request disconnects.
     *
     * @param ex The exception that caused the disconnection. May be null.
     */
    private void internalOnDisconnect(Exception ex) {
        if (!onDisconnect.isEmpty()) {
            for (ISubscriptionErrorDelegate disconnect : onDisconnect) {
                disconnect.subscriptionErrorDelegate(this, new SubscriptionErrorEventArgs(null, ex));
            }
        }
        this.currentHangingRequest = null;
    }

    /**
     * Gets a value indicating whether this connection is opened
     *
     * @throws Exception
     */
    public boolean getIsOpen() throws Exception {

        this.throwIfDisposed();
        if (this.currentHangingRequest == null) {
            return false;
        } else {
            return this.currentHangingRequest.isConnected();
        }

    }

    /**
     * Validates the state of the connection.
     *
     * @param isConnectedExpected Value indicating whether we expect to be currently connected.
     * @param errorMessage        The error message.
     * @throws Exception
     */
    private void validateConnectionState(boolean isConnectedExpected, String errorMessage) throws Exception {
        if ((isConnectedExpected && !this.getIsOpen()) || (!isConnectedExpected && this.getIsOpen())) {
            throw new ServiceLocalException(errorMessage);
        }
    }

    /**
     * Handles the service response object.
     *
     * @param response The response.
     * @throws ArgumentException
     */
    private void handleServiceResponseObject(Object response) throws ArgumentException {
        GetStreamingEventsResponse gseResponse = (GetStreamingEventsResponse) response;

        if (gseResponse == null) {
            throw new ArgumentNullException("GetStreamingEventsResponse must not be null",
                    "GetStreamingEventsResponse");
        } else {
            if (gseResponse.getResult() == ServiceResult.Success
                    || gseResponse.getResult() == ServiceResult.Warning) {
                if (gseResponse.getResults().getNotifications().size() > 0) {
                    // We got notification; dole them out.
                    this.issueNotificationEvents(gseResponse);
                } else {
                    // // This was just a heartbeat, nothing to do here.
                }
            } else if (gseResponse.getResult() == ServiceResult.Error) {
                if (gseResponse.getErrorSubscriptionIds() == null
                        || gseResponse.getErrorSubscriptionIds().size() == 0) {
                    // General error
                    this.issueGeneralFailure(gseResponse);
                } else {
                    // subscription-specific errors
                    this.issueSubscriptionFailures(gseResponse);
                }
            }
        }
    }

    /**
     * Issues the subscription failures.
     *
     * @param gseResponse The GetStreamingEvents response.
     */
    private void issueSubscriptionFailures(GetStreamingEventsResponse gseResponse) {
        ServiceResponseException exception = new ServiceResponseException(gseResponse);

        for (String id : gseResponse.getErrorSubscriptionIds()) {
            StreamingSubscription subscription = null;

            synchronized (this) {
                // Client can do any good or bad things in the below event
                // handler
                if (this.subscriptions != null && this.subscriptions.containsKey(id)) {
                    subscription = this.subscriptions.get(id);
                }

            }
            if (subscription != null) {
                SubscriptionErrorEventArgs eventArgs = new SubscriptionErrorEventArgs(subscription, exception);

                if (!onSubscriptionError.isEmpty()) {
                    for (ISubscriptionErrorDelegate subError : onSubscriptionError) {
                        subError.subscriptionErrorDelegate(this, eventArgs);
                    }
                }
            }
            if (gseResponse.getErrorCode() != ServiceError.ErrorMissedNotificationEvents) {
                // Client can do any good or bad things in the above event
                // handler
                synchronized (this) {
                    if (this.subscriptions != null && this.subscriptions.containsKey(id)) {
                        // We are no longer servicing the subscription.
                        this.subscriptions.remove(id);
                    }
                }
            }
        }
    }

    /**
     * Issues the general failure.
     *
     * @param gseResponse The GetStreamingEvents response.
     */
    private void issueGeneralFailure(GetStreamingEventsResponse gseResponse) {
        SubscriptionErrorEventArgs eventArgs = new SubscriptionErrorEventArgs(null,
                new ServiceResponseException(gseResponse));

        if (!onSubscriptionError.isEmpty()) {
            for (ISubscriptionErrorDelegate subError : onSubscriptionError) {
                subError.subscriptionErrorDelegate(this, eventArgs);
            }
        }
    }

    /**
     * Issues the notification events.
     *
     * @param gseResponse The GetStreamingEvents response.
     */
    private void issueNotificationEvents(GetStreamingEventsResponse gseResponse) {

        for (GetStreamingEventsResults.NotificationGroup events : gseResponse.getResults().getNotifications()) {
            StreamingSubscription subscription = null;

            synchronized (this) {
                // Client can do any good or bad things in the below event
                // handler
                if (this.subscriptions != null && this.subscriptions.containsKey(events.subscriptionId)) {
                    subscription = this.subscriptions.get(events.subscriptionId);
                }
            }
            if (subscription != null) {
                NotificationEventArgs eventArgs = new NotificationEventArgs(subscription, events.events);

                if (!onNotificationEvent.isEmpty()) {
                    for (INotificationEventDelegate notifyEvent : onNotificationEvent) {
                        notifyEvent.notificationEventDelegate(this, eventArgs);
                    }
                }
            }
        }
    }

    /**
     * Frees resources associated with this StreamingSubscriptionConnection.
     */
    public void dispose() {
        synchronized (this) {
            if (!this.isDisposed) {
                if (this.currentHangingRequest != null) {
                    this.currentHangingRequest = null;
                }

                this.subscriptions = null;
                this.session = null;

                this.isDisposed = true;
            }
        }
    }

    /**
     * Throws if disposed.
     *
     * @throws Exception
     */
    private void throwIfDisposed() throws Exception {
        if (this.isDisposed) {
            throw new Exception(this.getClass().getName());
        }
    }

    @Override
    public void handleResponseObject(Object response) throws ArgumentException {
        this.handleServiceResponseObject(response);
    }

    @Override
    public void hangingRequestDisconnectHandler(Object sender, HangingRequestDisconnectEventArgs args) {
        this.onRequestDisconnect(sender, args);
    }

}