using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Diagnostics; using System.Threading; using WindowsPhone.Recipes.Push.Messasges.Properties; namespace WindowsPhone.Recipes.Push.Messasges { /// /// Represents a base class for push notification messages. /// /// /// This class members are thread safe. /// public abstract class PushNotificationMessage { #region Constants /// Push notification maximum message size including headers and payload. protected const int MaxMessageSize = 1024; /// /// Well known push notification message web request headers. /// internal static class Headers { public const string MessageId = "X-MessageID"; public const string BatchingInterval = "X-NotificationClass"; public const string NotificationStatus = "X-NotificationStatus"; public const string DeviceConnectionStatus = "X-DeviceConnectionStatus"; public const string SubscriptionStatus = "X-SubscriptionStatus"; public const string WindowsPhoneTarget = "X-WindowsPhone-Target"; } #endregion #region Fields /// Synchronizes payload manipulations. private readonly object _sync = new object(); /// The payload raw bytes of this message. private byte[] _payload; private MessageSendPriority _sendPriority; #endregion #region Properties /// /// Gets this message unique ID. /// public Guid Id { get; private set; } /// /// Gets or sets the send priority of this message in the MPNS. /// public MessageSendPriority SendPriority { get { return _sendPriority; } set { SafeSet(ref _sendPriority, value); } } /// /// Gets or sets the message payload. /// protected byte[] Payload { get { return _payload; } set { SafeSet(ref _payload, value); } } protected abstract int NotificationClassId { get; } /// /// Gets or sets the flag indicating that one of the message properties /// has changed, thus the payload should be rebuilt. /// private bool IsDirty { get; set; } #endregion #region Ctor /// /// Initializes a new instance of this type with send priority. /// protected PushNotificationMessage(MessageSendPriority sendPriority = MessageSendPriority.Normal) { Id = Guid.NewGuid(); SendPriority = sendPriority; IsDirty = true; } #endregion #region Operations /// /// Synchronously send this messasge to the destination address. /// /// /// Note that properties of this instance may be changed by different threads while /// sending, but once the payload created, it won't be changed until the next send. /// /// Destination address uri. /// One of the arguments is null. /// Payload size is out of range. For maximum allowed message size see /// Failed to send message for any reason. /// The result instance with relevant information for this send operation. public MessageSendResult Send(Uri uri) { Guard.ArgumentNotNull(uri, "uri"); // Create payload or reuse cached one. var payload = GetOrCreatePayload(); // Create and initialize the request object. var request = CreateWebRequest(uri, payload); var result = SendSynchronously(payload, uri, request); return result; } /// /// Asynchronously send this messasge to the destination address. /// /// /// This method uses the .NET Thread Pool. Use this method to send one or few /// messages asynchronously. If you have many messages to send, please consider /// of using the synchronous method with custom (external) queue-thread solution. /// /// Note that properties of this instance may be changed by different threads while /// sending, but once the payload created, it won't be changed until the next send. /// /// Destination address uri. /// Message sent callback. /// Message send error callback. /// One of the arguments is null. /// Payload size is out of range. For maximum allowed message size see public void SendAsync(Uri uri, Action messageSent = null, Action messageError = null) { Guard.ArgumentNotNull(uri, "uri"); // Create payload or reuse cached one. var payload = GetOrCreatePayload(); // Create and initialize the request object. var request = CreateWebRequest(uri, payload); SendAsynchronously( payload, uri, request, messageSent ?? (result => { }), messageError ?? (result => { })); } #endregion #region Protected & Virtuals /// /// Override to create the message payload. /// /// The messasge payload bytes. protected virtual byte[] OnCreatePayload() { return _payload; } /// /// Override to initialize the message web request with custom headers. /// /// The message web request. protected virtual void OnInitializeRequest(HttpWebRequest request) { } /// /// Check the size of the payload and reject it if too big. /// /// Payload raw bytes. protected abstract void VerifyPayloadSize(byte[] payload); /// /// Safely set oldValue with newValue in case that are different, and raise the dirty flag. /// /// The type of the value. /// The old value. /// The new value. protected void SafeSet(ref T oldValue, T newValue) { lock (_sync) { if (!object.Equals(oldValue, newValue)) { oldValue = newValue; IsDirty = true; } } } #endregion #region Privates /// /// Synchronously send this message to the destination uri. /// /// The message payload bytes. /// The message destination uri. /// Initialized Web request instance. /// The result instance with relevant information for this send operation. private MessageSendResult SendSynchronously(byte[] payload, Uri uri, HttpWebRequest request) { try { // Get the request stream. using (var requestStream = request.GetRequestStream()) { // Start to write the payload to the stream. requestStream.Write(payload, 0, payload.Length); // Switch to receiving the response from MPNS. using (var response = (HttpWebResponse)request.GetResponse()) { var result = new MessageSendResult(this, uri, response); if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidOperationException(string.Format(Resources.ServerErrorStatusCode, response.StatusCode)); } return result; } } } catch (WebException ex) { var result = new MessageSendResult(this, uri, ex); throw new MessageSendException(result, ex); } catch (Exception ex) { var result = new MessageSendResult(this, uri, ex); throw new MessageSendException(result, ex); } } /// /// Asynchronously send this message to the destination uri using the HttpWebRequest context. /// /// The message payload bytes. /// The message destination uri. /// Initialized Web request instance. /// Message sent callback. /// Message send error callback. /// The result instance with relevant information for this send operation. private void SendAsynchronously(byte[] payload, Uri uri, HttpWebRequest request, Action sent, Action error) { try { // Get the request stream asynchronously. request.BeginGetRequestStream(requestAsyncResult => { try { using (var requestStream = request.EndGetRequestStream(requestAsyncResult)) { // Start writing the payload to the stream. requestStream.Write(payload, 0, payload.Length); } // Switch to receiving the response from MPNS asynchronously. request.BeginGetResponse(responseAsyncResult => { try { using (var response = (HttpWebResponse)request.EndGetResponse(responseAsyncResult)) { var result = new MessageSendResult(this, uri, response); if (response.StatusCode == HttpStatusCode.OK) { sent(result); } else { error(result); } } } catch (Exception ex3) { error(new MessageSendResult(this, uri, ex3)); } }, null); } catch (Exception ex2) { error(new MessageSendResult(this, uri, ex2)); } }, null); } catch (Exception ex1) { error(new MessageSendResult(this, uri, ex1)); } } /// /// Create a payload and verify its size. /// /// Payload raw bytes. private byte[] GetOrCreatePayload() { if (IsDirty) { lock (_sync) { if (IsDirty) { var payload = OnCreatePayload() ?? new byte[0]; DebugOutput(payload); VerifyPayloadSize(payload); _payload = payload; IsDirty = false; } } } return _payload; } private HttpWebRequest CreateWebRequest(Uri uri, byte[] payload) { var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml; charset=utf-8"; request.ContentLength = payload.Length; request.Headers[Headers.MessageId] = Id.ToString(); // Batching interval is composed of the message priority and the message class id. int batchingInterval = ((int)SendPriority * 10) + NotificationClassId; request.Headers[Headers.BatchingInterval] = batchingInterval.ToString(); OnInitializeRequest(request); return request; } #endregion #region Diagnostics [Conditional("DEBUG")] private static void DebugOutput(byte[] payload) { string payloadString = Encoding.ASCII.GetString(payload); Debug.WriteLine(payloadString); } #endregion } }