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
}
}