All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.windowsazure.messaging.NotificationHub Maven / Gradle / Ivy

Go to download

Azure Notification Hubs Java SDK for interacting with the data and management plane operations.

The newest version!
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------

package com.windowsazure.messaging;

import com.google.gson.GsonBuilder;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.entity.mime.FormBodyPart;
import org.apache.hc.client5.http.entity.mime.FormBodyPartBuilder;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.entity.mime.StringBody;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class represents all actions that can be done on an Azure Notification Hub.
 */
public class NotificationHub extends NotificationHubsService implements NotificationHubClient {

    private static final String API_VERSION = "?api-version=2020-06";
    private static final String CONTENT_LOCATION_HEADER = "Location";
    private String endpoint;
    private final String hubPath;

    /**
     * Creates a new instance of the NotificationHub class with connection string and hub path.
     * @param connectionString The connection string from the Azure Notification Hub access policies.
     * @param hubPath The name of the Azure Notification Hub name.
     */
    public NotificationHub(String connectionString, String hubPath) {
        this.hubPath = hubPath;

        String sasKeyName = null;
        String sasKeyValue = null;

        String[] parts = connectionString.split(";");
        if (parts.length != 3)
            throw new RuntimeException("Error parsing connection string: "
                + connectionString);

        for (String part : parts) {
            if (part.startsWith("Endpoint")) {
                this.endpoint = "https" + part.substring(11);
            } else if (part.startsWith("SharedAccessKeyName")) {
                sasKeyName = part.substring(20);
            } else if (part.startsWith("SharedAccessKey")) {
                sasKeyValue = part.substring(16);
            }
        }

        tokenProvider = new SasTokenProvider(sasKeyName, sasKeyValue);
    }

    /**
     * This method creates a new registration
     *
     * @param registration A registration object containing the description of the
     *                     registration to create. ETag and registration ID are
     *                     ignored
     * @param callback     A callback when invoked returns created registration
     *                     containing the read-only parameters (registration ID,
     *                     ETag, and expiration time)
     */
    @Override
    public  void createRegistrationAsync(T registration, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrations" + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .setBody(registration.getXml(), ContentType.APPLICATION_ATOM_XML)
            .build();

        executeRequest(post, callback, 200, response -> {
            try {
                callback.completed(Registration.parse(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * This method creates a new registration
     *
     * @param registration A registration object containing the description of the
     *                     registration to create. ETag and registration ID are
     *                     ignored
     * @return The created registration containing the read-only parameters
     * (registration ID, ETag, and expiration time).
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public  T createRegistration(T registration) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        createRegistrationAsync(registration, callback);
        return callback.getResult();
    }

    /**
     * Create a registrationId, without creating an actual registration. To create
     * use upsert. This method is used when the registration id is stored only on
     * the device.
     *
     * @param callback A callback with the newly created registration ID.
     */
    @Override
    public void createRegistrationIdAsync(final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrationids" + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .build();

        executeRequest(post, callback, 201, response -> {
            String location = response.getFirstHeader(CONTENT_LOCATION_HEADER).getValue();
            Pattern extractId = Pattern.compile("(\\S+)/registrationids/([^?]+).*");
            Matcher m = extractId.matcher(location);

            if (m.matches()) {
                String id = m.group(2);
                callback.completed(id);
            } else {
                callback.completed(null);
            }
        });
    }

    /**
     * Create a registrationId, without creating an actual registration. To create
     * use upsert. This method is used when the registration id is stored only on
     * the device.
     *
     * @return The newly created registration ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public String createRegistrationId() throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        createRegistrationIdAsync(callback);
        return callback.getResult();
    }

    /**
     * This method updates an existing registration
     *
     * @param registration A registration object containing the description of the
     *                     registration to update. The registration ID must be
     *                     populated.
     * @param callback     A callback when invoked, returns the updated registration
     *                     containing the read-only parameters (registration ID,
     *                     ETag, and expiration time).
     * @param  The type of Registration class.
     */
    @Override
    public  void updateRegistrationAsync(T registration, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrations/" + registration.getRegistrationId() + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest put = createRequest(uri, Method.PUT)
            .setHeader("If-Match", registration.getEtag() == null ? "*" : "W/\"" + registration.getEtag() + "\"")
            .setBody(registration.getXml(), ContentType.APPLICATION_ATOM_XML)
            .build();

        executeRequest(put, callback, 200, response -> {
            try {
                callback.completed(Registration.parse(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * This method updates an existing registration
     *
     * @param registration A registration object containing the description of the
     *                     registration to update. The registration ID has to be
     *                     populated.
     * @return The updated registration containing the read-only parameters
     * (registration ID, ETag, and expiration time).
     * @param  The type of Registration class.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public  T updateRegistration(T registration) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        updateRegistrationAsync(registration, callback);
        return callback.getResult();
    }

    /**
     * This method updates or creates a new registration with the registration id
     * specified.
     *
     * @param registration A registration object containing the description of the
     *                     registration to create or update. The registration ID
     *                     must be populated.
     * @param callback     A callback, when invoked, returns the updated
     *                     registration containing the read-only parameters
     *                     (registration ID, ETag, and expiration time).
     * @param  The type of Registration class.
     */
    @Override
    public  void upsertRegistrationAsync(T registration, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrations/" + registration.getRegistrationId() + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest put = createRequest(uri, Method.PUT)
            .setBody(registration.getXml(), ContentType.APPLICATION_ATOM_XML)
            .build();

        executeRequest(put, callback, 200, response -> {
            try {
                callback.completed(Registration.parse(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * This method updates or creates a new registration with the registration ID
     * specified.
     *
     * @param registration A registration object containing the description of the
     *                     registration to create or update. The registration ID
     *                     must be populated.
     * @return The updated registration containing the read-only parameters
     * (registration ID, ETag, and expiration time).
     * @throws NotificationHubsException Thrown if there is a client error.
     * @param  The type of Registration class.
     */
    @Override
    public  T upsertRegistration(T registration) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        upsertRegistrationAsync(registration, callback);
        return callback.getResult();
    }

    /**
     * Deletes a registration with the given registration containing a populated
     * registrationId.
     *
     * @param registration The registration containing the registrationId field
     *                     populated.
     * @param callback     A callback when invoked returns nothing.
     */
    @Override
    public void deleteRegistrationAsync(Registration registration, final FutureCallback callback) {
        deleteRegistrationAsync(registration.getRegistrationId(), callback);
    }

    /**
     * Deletes a registration with the given registration containing a populated
     * registrationId.
     *
     * @param registration The registration containing the registrationId field
     *                     populated.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void deleteRegistration(Registration registration) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        deleteRegistrationAsync(registration, callback);
        callback.getResult();
    }

    /**
     * Deletes a registration by the given registration ID.
     *
     * @param registrationId The registration ID for the registration to delete.
     * @param callback       A callback when invoked returns nothing.
     */
    @Override
    public void deleteRegistrationAsync(String registrationId, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrations/" + registrationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest delete = createRequest(uri, Method.DELETE)
            .setHeader("If-Match", "*")
            .build();

        executeRequest(delete, callback, new int[] { 200, 404 }, response -> callback.completed(null));
    }

    /**
     * Deletes a registration by the given registration ID.
     *
     * @param registrationId The registration ID for the registration to delete.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void deleteRegistration(String registrationId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        deleteRegistrationAsync(registrationId, callback);
        callback.getResult();
    }

    /**
     * Retrieves the description of a registration based on the ID.
     *
     * @param registrationId The ID for the registration to retrieve.
     * @param callback       A callback, when invoked, returns the registration with
     *                       the ID matching the given registration ID.
     * @param  The type of Registration class.
     */
    @Override
    public  void getRegistrationAsync(String registrationId, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + "/registrations/" + registrationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            try {
                callback.completed(Registration.parse(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Retrieves the description of a registration based on the ID.
     *
     * @param registrationId The ID for the registration to retrieve.
     * @return The registration with the ID matching the given registration ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     * @param  The type of Registration class.
     */
    @Override
    public  T getRegistration(String registrationId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationAsync(registrationId, callback);
        return callback.getResult();
    }

    /**
     * Return all registrations in the current notification hub.
     *
     * @param callback The callback when invoked, returns a collection containing
     *                 registrations.
     */
    @Override
    public void getRegistrationsAsync(FutureCallback callback) {
        getRegistrationsAsync(0, null, callback);
    }

    /**
     * Returns all registrations in this hub
     *
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @param callback          A callback when invoked returns a collection
     *                          containing the registrations.
     */
    @Override
    public void getRegistrationsAsync(int top, String continuationToken, final FutureCallback callback) {
        String queryUri = endpoint + hubPath + "/registrations" + API_VERSION + getQueryString(top, continuationToken);
        retrieveRegistrationCollectionAsync(queryUri, callback);
    }

    /**
     * Returns all registrations in this hub
     *
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @return A collection containing the registrations.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrations(int top, String continuationToken) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationsAsync(top, continuationToken, callback);
        return callback.getResult();
    }

    /**
     * Return all registrations in the current notification hub.
     *
     * @return Collection containing the registrations.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrations() throws NotificationHubsException {
        return getRegistrations(0, null);
    }

    /**
     * Returns all registrations with a specific tag
     *
     * @param tag               The tag to search for registrations.
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @param callback          A callback when invoked, returns a collection of
     *                          registrations with the given tag.
     */
    @Override
    public void getRegistrationsByTagAsync(String tag, int top, String continuationToken, final FutureCallback callback) {
        String queryUri = endpoint + hubPath + "/tags/" + tag
            + "/registrations" + API_VERSION
            + getQueryString(top, continuationToken);
        retrieveRegistrationCollectionAsync(queryUri, callback);
    }

    /**
     * Returns all registrations with a specific tag
     *
     * @param tag               The tag to search for registrations.
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @return A collection of registrations with the given tag.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrationsByTag(String tag, int top, String continuationToken) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationsByTagAsync(tag, top, continuationToken, callback);
        return callback.getResult();
    }

    /**
     * Returns all registrations with a specific tag
     *
     * @param tag      The tag to search for registrations.
     * @param callback A callback, when invoked, returns a collection of
     *                 registrations with the given tag.
     */
    @Override
    public void getRegistrationsByTagAsync(String tag, final FutureCallback callback) {
        getRegistrationsByTagAsync(tag, 0, null, callback);
    }

    /**
     * Returns all registrations with a specific tag
     *
     * @param tag The tag to search for registrations.
     * @return A collection of registrations with the given tag.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrationsByTag(String tag) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationsByTagAsync(tag, callback);
        return callback.getResult();
    }

    /**
     * Returns all registration with a specific channel (e.g. ChannelURI, device
     * token)
     *
     * @param channel           The channel URI, device token or other unique PNS
     *                          identifier.
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @param callback          A callback, when invoked, returns a collection of
     *                          registrations with matching channels.
     */
    @Override
    public void getRegistrationsByChannelAsync(String channel, int top, String continuationToken, final FutureCallback callback) {
        String queryUri;
        try {
            String channelQuery = URLEncoder.encode("ChannelUri eq '" + channel + "'", "UTF-8");
            queryUri = endpoint + hubPath + "/registrations" + API_VERSION
                + "&$filter=" + channelQuery
                + getQueryString(top, continuationToken);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        retrieveRegistrationCollectionAsync(queryUri, callback);
    }

    /**
     * Returns all registration with a specific channel (e.g. ChannelURI, device
     * token)
     *
     * @param channel           The channel URI, device token or other unique PNS
     *                          identifier.
     * @param top               The maximum number of registrations to return (max
     *                          100)
     * @param continuationToken If not-null, continues iterating through a
     *                          previously requested query.
     * @return A collection of registrations with matching channels.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrationsByChannel(String channel, int top, String continuationToken) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationsByChannelAsync(channel, top, continuationToken, callback);
        return callback.getResult();
    }

    /**
     * Returns all registration with a specific channel (e.g. ChannelURI, device
     * token)
     *
     * @param channel  The channel URI, device token or other unique PNS identifier.
     * @param callback A callback, when invoked, returns a collection of
     *                 registrations with matching channels.
     */
    @Override
    public void getRegistrationsByChannelAsync(String channel, final FutureCallback callback) {
        getRegistrationsByChannelAsync(channel, 0, null, callback);
    }

    /**
     * Returns all registration with a specific channel (e.g. ChannelURI, device
     * token)
     *
     * @param channel The channel URI, device token or other unique PNS identifier.
     * @return A collection of registrations with matching channels.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public CollectionResult getRegistrationsByChannel(String channel) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getRegistrationsByChannelAsync(channel, callback);
        return callback.getResult();
    }

    private String getQueryString(int top, String continuationToken) {
        StringBuilder buf = new StringBuilder();
        if (top > 0) {
            buf.append("&$top=").append(top);
        }
        if (continuationToken != null) {
            buf.append("&ContinuationToken=").append(continuationToken);
        }
        return buf.toString();
    }

    private void retrieveRegistrationCollectionAsync(String queryUri, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(queryUri);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            CollectionResult result;
            try {
                result = Registration.parseRegistrations(response.getBodyBytes());
            } catch (Exception e) {
                callback.failed(e);
                return;
            }

            Header contTokenHeader = response.getFirstHeader("X-MS-ContinuationToken");
            if (contTokenHeader != null) {
                result.setContinuationToken(contTokenHeader.getValue());
            }

            callback.completed(result);
        });
    }

    /**
     * Sends a notification to all eligible registrations (i.e. only correct
     * platform, if notification is platform specific)
     *
     * @param notification The notification to send to all eligible registrations.
     * @param callback     A callback, when invoked, returns a notification outcome
     *                     with the tracking ID and notification ID.
     */
    @Override
    public void sendNotificationAsync(Notification notification, FutureCallback callback) {
        scheduleNotificationAsync(notification, "", null, callback);
    }

    /**
     * Sends a notification to all eligible registrations (i.e. only correct
     * platform, if notification is platform specific)
     *
     * @param notification The notification to send to all eligible registrations.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome sendNotification(Notification notification) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        sendNotificationAsync(notification, callback);
        return callback.getResult();
    }

    /**
     * Sends a notifications to all eligible registrations with at least one of the
     * specified tags
     *
     * @param notification The notification to send to the audience with the
     *                     specified tags.
     * @param tags         The tags for targeting the notifications.
     * @param callback     A callback, when invoked, returns a notification outcome
     *                     with the tracking ID and notification ID.
     */
    @Override
    public void sendNotificationAsync(Notification notification, Set tags, FutureCallback callback) {
        scheduleNotificationAsync(notification, tags, null, callback);
    }

    /**
     * Sends a notifications to all eligible registrations with at least one of the
     * specified tags
     *
     * @param notification The notification to send to the audience with the
     *                     specified tags.
     * @param tags         The tags for targeting the notifications.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome sendNotification(Notification notification, Set tags) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        sendNotificationAsync(notification, tags, callback);
        return callback.getResult();
    }

    /**
     * Sends a notifications to all eligible registrations that satisfy the provided
     * tag expression.
     *
     * @param notification  The notification to send to the audience that matches
     *                      the specified tag expression.
     * @param tagExpression The tag expression for targeting the notifications.
     * @param callback      A callback, when invoked, returns a notification outcome
     *                      with the tracking ID and notification ID.
     */
    @Override
    public void sendNotificationAsync(Notification notification, String tagExpression, FutureCallback callback) {
        scheduleNotificationAsync(notification, tagExpression, null, callback);
    }

    /**
     * Sends a notifications to all eligible registrations that satisfy the provided
     * tag expression.
     *
     * @param notification  The notification to send to the audience that matches
     *                      the specified tag expression.
     * @param tagExpression The tag expression for targeting the notifications.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome sendNotification(Notification notification, String tagExpression) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        sendNotificationAsync(notification, tagExpression, callback);
        return callback.getResult();
    }

    /**
     * Schedules a notification at the given scheduled time.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the scheduled time.
     * @param scheduledTime The scheduled time for the notification.
     * @param callback      A callback, when invoked, returns a notification outcome
     *                      with the tracking ID and notification ID.
     */
    @Override
    public void scheduleNotificationAsync(Notification notification, Date scheduledTime, FutureCallback callback) {
        scheduleNotificationAsync(notification, "", scheduledTime, callback);
    }

    /**
     * Schedules a notification at the given scheduled time.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the scheduled time.
     * @param scheduledTime The scheduled time for the notification.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome scheduleNotification(Notification notification, Date scheduledTime) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        scheduleNotificationAsync(notification, scheduledTime, callback);
        return callback.getResult();
    }

    /**
     * Schedules a notification at the given time with a set of tags.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the given time.
     * @param tags          The tags associated with the notification targeting.
     * @param scheduledTime The scheduled time for the notification.
     * @param callback      A callback, when invoked, returns a notification outcome
     *                      with the tracking ID and notification ID.
     */
    @Override
    public void scheduleNotificationAsync(Notification notification, Set tags, Date scheduledTime, FutureCallback callback) {
        if (tags.isEmpty()) {
            throw new IllegalArgumentException("tags has to contain at least an element");
        }

        StringBuilder exp = new StringBuilder();
        for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) {
            exp.append(iterator.next());
            if (iterator.hasNext())
                exp.append(" || ");
        }

        scheduleNotificationAsync(notification, exp.toString(), scheduledTime, callback);
    }

    /**
     * Schedules a notification at the given time with a set of tags.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the given time.
     * @param tags          The tags associated with the notification targeting.
     * @param scheduledTime The scheduled time for the notification.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome scheduleNotification(Notification notification, Set tags, Date scheduledTime) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        scheduleNotificationAsync(notification, tags, scheduledTime, callback);
        return callback.getResult();
    }

    /**
     * Schedules a notification at the given time with a tag expression.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the given time.
     * @param tagExpression The tag expression associated with the notification
     *                      targeting.
     * @param scheduledTime The scheduled time for the notification.
     * @param callback      A callback, when invoked, returns a notification outcome
     *                      with the tracking ID and notification ID.
     */
    @Override
    public void scheduleNotificationAsync(Notification notification, String tagExpression, Date scheduledTime, final FutureCallback callback) {
        URI uri;
        try {
            uri = new URI(endpoint + hubPath + (scheduledTime == null ? "/messages" : "/schedulednotifications") + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .setBody(notification.getBody(), notification.getContentType())
            .build();

        if (scheduledTime != null) {
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            df.setTimeZone(TimeZone.getTimeZone("UTC"));
            String scheduledTimeHeader = df.format(scheduledTime);
            post.setHeader("ServiceBusNotification-ScheduleTime", scheduledTimeHeader);
        }

        if (tagExpression != null && !"".equals(tagExpression)) {
            post.setHeader("ServiceBusNotification-Tags", tagExpression);
        }

        for (String header : notification.getHeaders().keySet()) {
            post.setHeader(header, notification.getHeaders().get(header));
        }

        executeRequest(post, callback, 201, response -> sendNotificationOutcome(callback, post, response));
    }

    private void sendNotificationOutcome(FutureCallback callback, SimpleHttpRequest post, SimpleHttpResponse response) {
        String trackingId = post.getFirstHeader(TRACKING_ID_HEADER).getValue();
        String notificationId = null;
        Header locationHeader = response.getFirstHeader(CONTENT_LOCATION_HEADER);
        if (locationHeader != null) {
            URI location;
            try {
                location = new URI(locationHeader.getValue());
            } catch (URISyntaxException e) {
                callback.failed(e);
                return;
            }
            String[] segments = location.getPath().split("/");
            notificationId = segments[segments.length - 1];
        }

        callback.completed(new NotificationOutcome(trackingId, notificationId));
    }

    /**
     * Schedules a notification at the given time with a tag expression.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send at the given time.
     * @param tagExpression The tag expression associated with the notification
     *                      targeting.
     * @param scheduledTime The scheduled time for the notification.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome scheduleNotification(Notification notification, String tagExpression, Date scheduledTime) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        scheduleNotificationAsync(notification, tagExpression, scheduledTime, callback);
        return callback.getResult();
    }

    /**
     * Cancels the scheduled notification with the given notification ID.
     *
     * @param notificationId The notification ID of the notification to cancel.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void cancelScheduledNotification(String notificationId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        cancelScheduledNotificationAsync(notificationId, callback);
        callback.getResult();
    }

    /**
     * Cancels the scheduled notification with the given notification ID.
     *
     * @param notificationId The notification ID of the notification to cancel.
     * @param callback       A callback, when invoked, returns nothing.
     */
    @Override
    public void cancelScheduledNotificationAsync(String notificationId, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/schedulednotifications/" + notificationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest delete = createRequest(uri, Method.DELETE)
            .build();

        executeRequest(delete, callback, new int[] { 200, 404 }, response -> callback.completed(null));
    }

    /**
     * Sends a direct notification to a given device handle.
     *
     * @param notification The notification to send directly to the device handle.
     * @param deviceHandle The device handle to target for the notification.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome sendDirectNotification(Notification notification, String deviceHandle) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        sendDirectNotificationAsync(notification, deviceHandle, callback);
        return callback.getResult();
    }

    /**
     * Sends a direct notification to the given device handles.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send directly to the device handles.
     * @param deviceHandles The device handles to target for the notification.
     * @return A notification outcome with the tracking ID and notification ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationOutcome sendDirectNotification(Notification notification, List deviceHandles) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        sendDirectNotificationAsync(notification, deviceHandles, callback);
        return callback.getResult();
    }

    /**
     * Sends a direct notification to a given device handle.
     *
     * @param notification The notification to send directly to the device handle.
     * @param deviceHandle The device handle to target for the notification.
     * @param callback     A callback, when invoked, returns a notification outcome
     *                     with the tracking ID and notification ID.
     */
    @Override
    public void sendDirectNotificationAsync(
        Notification notification,
        String deviceHandle,
        final FutureCallback callback
    ) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/messages" + API_VERSION + "&direct");
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .setHeader("ServiceBusNotification-DeviceHandle", deviceHandle)
            .setBody(notification.getBody(), notification.getContentType())
            .build();

        for (String header : notification.getHeaders().keySet()) {
            post.setHeader(header, notification.getHeaders().get(header));
        }

        executeRequest(post, callback, 201, response -> sendNotificationOutcome(callback, post, response));
    }

    /**
     * Sends a direct notification to the given device handles.  Note that this is not available on the free SKU.
     *
     * @param notification  The notification to send directly to the device handles.
     * @param deviceHandles The device handles to target for the notification.
     * @param callback      A callback, when invoked, returns a notification outcome
     *                      with the tracking ID and notification ID.
     */
    @Override
    public void sendDirectNotificationAsync(Notification notification, List deviceHandles, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/messages/$batch" + API_VERSION + "&direct");
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .build();

        for (String header : notification.getHeaders().keySet()) {
            post.setHeader(header, notification.getHeaders().get(header));
        }

        FormBodyPart notificationPart = FormBodyPartBuilder.create()
            .setName("notification")
            .addField("Content-Disposition", "inline; name=notification")
            .setBody(new StringBody(notification.getBody(), notification.getContentType()))
            .build();

        String deviceHandlesJson = new GsonBuilder().disableHtmlEscaping().create().toJson(deviceHandles);
        FormBodyPart devicesPart = FormBodyPartBuilder.create()
            .setName("devices")
            .addField("Content-Disposition", "inline; name=devices")
            .setBody(new StringBody(deviceHandlesJson, ContentType.APPLICATION_JSON))
            .build();

        HttpEntity entity = MultipartEntityBuilder.create()
            .setMimeSubtype("mixed")
            .setBoundary("nh-batch-multipart-boundary")
            .addPart(notificationPart)
            .addPart(devicesPart)
            .build();

        ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
        try {
            entity.writeTo(baoStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        post.setBody(baoStream.toByteArray(), ContentType.parse(entity.getContentType()));

        executeRequest(post, callback, 201, response -> sendNotificationOutcome(callback, post, response));
    }

    /**
     * Gets notification telemetry by the notification ID.
     *
     * @param notificationId The notification ID for the telemetry.
     * @return The notification telemetry for the given notification.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationTelemetry getNotificationTelemetry(String notificationId)
        throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getNotificationTelemetryAsync(notificationId, callback);
        return callback.getResult();
    }

    /**
     * Gets notification telemetry by the notification ID.
     *
     * @param notificationId The notification ID for the telemetry.
     * @param callback       A callback, when invoked, returns the notification
     *                       telemetry for the given notification.
     */
    @Override
    public void getNotificationTelemetryAsync(String notificationId, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/messages/" + notificationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            try {
                callback.completed(NotificationTelemetry.parseOne(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Creates or updates an installation.
     *
     * @param installation The installation to create or update.
     * @param callback     A callback, when invoked, returns nothing.
     */
    @Override
    public void createOrUpdateInstallationAsync(BaseInstallation installation, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/installations/" + installation.getInstallationId() + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest put = createRequest(uri, Method.PUT)
            .setBody(installation.toJson(), ContentType.APPLICATION_JSON)
            .build();

        executeRequest(put, callback, 200, response -> callback.completed(null));
    }

    /**
     * Creates or updates an installation.
     *
     * @param installation The installation to create or update.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void createOrUpdateInstallation(BaseInstallation installation) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        createOrUpdateInstallationAsync(installation, callback);
        callback.getResult();
    }

    /**
     * Patches an installation with the given installation ID.
     *
     * @param installationId The installation ID to patch.
     * @param callback       A callback, when invoked, returns nothing.
     * @param operations     The list of operations to perform on the installation.
     */
    @Override
    public void patchInstallationAsync(String installationId, FutureCallback callback, PartialUpdateOperation... operations) {
        patchInstallationInternalAsync(installationId, PartialUpdateOperation.toJson(operations), callback);
    }

    /**
     * Patches an installation with the given installation ID.
     *
     * @param installationId The installation ID to patch.
     * @param operations     The list of operations to perform on the installation.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void patchInstallation(String installationId, PartialUpdateOperation... operations) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        patchInstallationAsync(installationId, callback, operations);
        callback.getResult();
    }

    /**
     * Patches an installation with the given installation ID.
     *
     * @param installationId The installation ID to patch.
     * @param operations     The list of operations to perform on the installation.
     * @param callback       A callback, when invoked, returns nothing.
     */
    @Override
    public void patchInstallationAsync(String installationId, List operations, FutureCallback callback) {
        patchInstallationInternalAsync(installationId, PartialUpdateOperation.toJson(operations), callback);
    }

    /**
     * Patches an installation with the given installation ID.
     *
     * @param installationId The installation ID to patch.
     * @param operations     The list of operations to perform on the installation.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void patchInstallation(String installationId, List operations) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        patchInstallationAsync(installationId, operations, callback);
        callback.getResult();
    }

    private void patchInstallationInternalAsync(String installationId, String operationsJson, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/installations/" + installationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest patch = createRequest(uri, Method.PATCH)
            .setBody(operationsJson, ContentType.APPLICATION_JSON)
            .build();

        executeRequest(patch, callback, 200, response -> callback.completed(null));
    }

    /**
     * Deletes an installation with the given installation ID.
     *
     * @param installationId The installation ID.
     * @param callback       A callback, when invoked, returns nothing.
     */
    @Override
    public void deleteInstallationAsync(String installationId, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/installations/" + installationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest delete = createRequest(uri, Method.DELETE)
            .build();

        executeRequest(delete, callback, 204, response -> callback.completed(null));
    }

    /**
     * Deletes an installation with the given installation ID.
     *
     * @param installationId The installation ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public void deleteInstallation(String installationId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        deleteInstallationAsync(installationId, callback);
        callback.getResult();
    }

    /**
     * Gets an installation by the given installation ID.
     *
     * @param installationId The installation ID for the installation to get.
     * @param callback       A callback, when invoked, returns the matching
     *                       installation by the installation ID.
     * @param  The type of Installation class.
     */
    @Override
    public  void getInstallationAsync(String installationId, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/installations/" + installationId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            try {
                callback.completed(BaseInstallation.fromJson(response.getBodyText()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Gets an installation by the given installation ID.
     *
     * @param installationId The installation ID for the installation to get.
     * @return The matching installation by the installation ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     * @param  The type of Installation class.
     */
    @Override
    public  T getInstallation(String installationId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getInstallationAsync(installationId, callback);
        return callback.getResult();
    }

    /**
     * Submits a notification hub job such as import or export. Note this is not available on the free or basic SKU.
     *
     * @param job      The notification hubs job to submit.
     * @param callback A callback, when invoked, returns the notification job with
     *                 status.
     */
    @Override
    public void submitNotificationHubJobAsync(NotificationHubJob job, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/jobs" + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest post = createRequest(uri, Method.POST)
            .setBody(job.getXml(), ContentType.APPLICATION_ATOM_XML)
            .build();

        executeRequest(post, callback, 201, response -> {
            try {
                callback.completed(NotificationHubJob.parseOne(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Submits a notification hub job such as import or export. Note this is not available on the free or basic SKU.
     *
     * @param job The notification hubs job to submit.
     * @return The notification job with status.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationHubJob submitNotificationHubJob(NotificationHubJob job) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        submitNotificationHubJobAsync(job, callback);
        return callback.getResult();
    }

    /**
     * Gets a notification hub job by the job ID.
     *
     * @param jobId    The job ID of the notification hub job to get.
     * @param callback A callback, when invoked, returns the notification hub job
     *                 with the matching job ID.
     */
    @Override
    public void getNotificationHubJobAsync(String jobId, final FutureCallback callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/jobs/" + jobId + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            try {
                callback.completed(NotificationHubJob.parseOne(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Gets a notification hub job by the job ID.
     *
     * @param jobId The job ID of the notification hub job to get.
     * @return The notification hub job with the matching job ID.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public NotificationHubJob getNotificationHubJob(String jobId) throws NotificationHubsException {
        SyncCallback callback = new SyncCallback<>();
        getNotificationHubJobAsync(jobId, callback);
        return callback.getResult();
    }

    /**
     * Gets all notification hub jobs for this namespace.
     *
     * @param callback A callback, when invoked, returns all notification hub jobs
     *                 for this namespace.
     */
    @Override
    public void getAllNotificationHubJobsAsync(final FutureCallback> callback) {
        URI uri;
        try  {
            uri = new URI(endpoint + hubPath + "/jobs" + API_VERSION);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        final SimpleHttpRequest get = createRequest(uri, Method.GET)
            .build();

        executeRequest(get, callback, 200, response -> {
            try {
                callback.completed(NotificationHubJob.parseCollection(response.getBodyBytes()));
            } catch (Exception e) {
                callback.failed(e);
            }
        });
    }

    /**
     * Gets all notification hub jobs for this namespace.
     *
     * @return All notification hub jobs for this namespace.
     * @throws NotificationHubsException Thrown if there is a client error.
     */
    @Override
    public List getAllNotificationHubJobs() throws NotificationHubsException {
        SyncCallback> callback = new SyncCallback<>();
        getAllNotificationHubJobsAsync(callback);
        return callback.getResult();
    }
}