Selaa lähdekoodia

[REVIEW] Promoting Zend_Service_WindowsAzure to trunk

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@19760 44c647ce-9c0f-0410-b52a-842ac1e357ba
matthew 16 vuotta sitten
vanhempi
commit
1e6d378bc2
50 muutettua tiedostoa jossa 12156 lisäystä ja 19 poistoa
  1. 4 0
      documentation/manual/en/manual.xml.in
  2. 115 0
      documentation/manual/en/module_specs/Zend_Service_WindowsAzure.xml
  3. 359 0
      documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Blob.xml
  4. 171 0
      documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Queue.xml
  5. 738 0
      documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Table.xml
  6. 184 0
      library/Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php
  7. 306 0
      library/Zend/Service/WindowsAzure/Credentials/SharedAccessSignature.php
  8. 154 0
      library/Zend/Service/WindowsAzure/Credentials/SharedKey.php
  9. 115 0
      library/Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php
  10. 35 0
      library/Zend/Service/WindowsAzure/Exception.php
  11. 36 0
      library/Zend/Service/WindowsAzure/RetryPolicy/Exception.php
  12. 58 0
      library/Zend/Service/WindowsAzure/RetryPolicy/NoRetry.php
  13. 92 0
      library/Zend/Service/WindowsAzure/RetryPolicy/RetryN.php
  14. 77 0
      library/Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php
  15. 217 0
      library/Zend/Service/WindowsAzure/SessionHandler.php
  16. 457 0
      library/Zend/Service/WindowsAzure/Storage.php
  17. 248 0
      library/Zend/Service/WindowsAzure/Storage/Batch.php
  18. 193 0
      library/Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php
  19. 1423 0
      library/Zend/Service/WindowsAzure/Storage/Blob.php
  20. 565 0
      library/Zend/Service/WindowsAzure/Storage/Blob/Stream.php
  21. 95 0
      library/Zend/Service/WindowsAzure/Storage/BlobContainer.php
  22. 116 0
      library/Zend/Service/WindowsAzure/Storage/BlobInstance.php
  23. 200 0
      library/Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php
  24. 556 0
      library/Zend/Service/WindowsAzure/Storage/Queue.php
  25. 91 0
      library/Zend/Service/WindowsAzure/Storage/QueueInstance.php
  26. 101 0
      library/Zend/Service/WindowsAzure/Storage/QueueMessage.php
  27. 94 0
      library/Zend/Service/WindowsAzure/Storage/SignedIdentifier.php
  28. 806 0
      library/Zend/Service/WindowsAzure/Storage/Table.php
  29. 323 0
      library/Zend/Service/WindowsAzure/Storage/TableEntity.php
  30. 326 0
      library/Zend/Service/WindowsAzure/Storage/TableEntityQuery.php
  31. 95 0
      library/Zend/Service/WindowsAzure/Storage/TableInstance.php
  32. 79 19
      tests/TestConfiguration.php.dist
  33. 2 0
      tests/Zend/Service/AllTests.php
  34. 91 0
      tests/Zend/Service/WindowsAzure/AllTests.php
  35. 223 0
      tests/Zend/Service/WindowsAzure/BlobStorageSharedAccessTest.php
  36. 514 0
      tests/Zend/Service/WindowsAzure/BlobStorageTest.php
  37. 314 0
      tests/Zend/Service/WindowsAzure/BlobStreamTest.php
  38. 64 0
      tests/Zend/Service/WindowsAzure/Credentials/AllTests.php
  39. 143 0
      tests/Zend/Service/WindowsAzure/Credentials/SharedAccessSignatureTest.php
  40. 174 0
      tests/Zend/Service/WindowsAzure/Credentials/SharedKeyLiteTest.php
  41. 171 0
      tests/Zend/Service/WindowsAzure/Credentials/SharedKeyTest.php
  42. 114 0
      tests/Zend/Service/WindowsAzure/DynamicTableEntityTest.php
  43. 326 0
      tests/Zend/Service/WindowsAzure/QueueStorageTest.php
  44. 119 0
      tests/Zend/Service/WindowsAzure/RetryPolicyTest.php
  45. 263 0
      tests/Zend/Service/WindowsAzure/SessionHandlerTest.php
  46. 75 0
      tests/Zend/Service/WindowsAzure/StorageTest.php
  47. 189 0
      tests/Zend/Service/WindowsAzure/TableEntityQueryTest.php
  48. 155 0
      tests/Zend/Service/WindowsAzure/TableEntityTest.php
  49. 790 0
      tests/Zend/Service/WindowsAzure/TableStorageTest.php
  50. BIN
      tests/Zend/Service/WindowsAzure/_files/WindowsAzure.gif

+ 4 - 0
documentation/manual/en/manual.xml.in

@@ -489,6 +489,10 @@
         <xi:include href="module_specs/Zend_Service_StrikeIron-AdvancedUses.xml" />
         <xi:include href="module_specs/Zend_Service_Technorati.xml" />
         <xi:include href="module_specs/Zend_Service_Twitter.xml" parse="xml" />
+        <xi:include href="module_specs/Zend_Service_WindowsAzure.xml" />
+        <xi:include href="module_specs/Zend_Service_WindowsAzure_Blob.xml" />
+        <xi:include href="module_specs/Zend_Service_WindowsAzure_Table.xml" />
+        <xi:include href="module_specs/Zend_Service_WindowsAzure_Queue.xml" />
         <xi:include href="module_specs/Zend_Service_Yahoo.xml" />
     </chapter>
 

+ 115 - 0
documentation/manual/en/module_specs/Zend_Service_WindowsAzure.xml

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.service.windowsazure">
+    <title>Zend_Service_WindowsAzure</title>
+
+    <sect2 id="zend.service.windowsazure.introduction">
+        <title>Introduction</title>
+
+        <para>
+          Windows Azure is the name for Microsoft’s Software + Services platform, an operating
+          system in the cloud providing services for hosting, management, scalable storage with
+          support for simple blobs, tables, and queues, as well as a management infrastructure for
+          provisioning and geo-distribution of cloud-based services, and a development platform for
+          the Azure Services layer.
+        </para>
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.sdk">
+        <title>Installing the Windows Azure SDK</title>
+
+        <para>
+            There are two development scenario's when working with Windows Azure.
+        </para>
+
+        <itemizedlist>
+            <listitem>
+                <para>
+                    You can develop your application using
+                    <classname>Zend_Service_WindowsAzure</classname> and the Windows Azure SDK,
+                    which provides a local developent environment of the services provided by
+                    Windows Azure's cloud infrastructure.
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    You can develop your application using
+                    <classname>Zend_Service_WindowsAzure</classname>, working directly with the
+                    Windows Azure cloud infrastructure.
+                </para>
+            </listitem>
+        </itemizedlist>
+
+        <para>
+            The first case requires you to install the <ulink
+                url="http://www.microsoft.com/downloads/details.aspx?familyid=11B451C4-7A7B-4537-A769-E1D157BAD8C6&amp;displaylang=en">Windows
+                Azure SDK</ulink> on your development machine.  It is currently only available for
+            Windows environments; progress is being made on a Java-based version of the SDK which
+            can run on any platform.
+        </para>
+
+        <para>
+            The latter case requires you to have an account at <ulink
+                url="http://www.azure.com">Azure.com</ulink>.
+        </para>
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.apiDocumentation">
+        <title>API Documentation</title>
+
+        <para>
+            The <classname>Zend_Service_WindowsAzure</classname> class provides the PHP wrapper to
+            the Windows Azure <acronym>REST</acronym> interface. Please consult the <ulink
+                url="http://msdn.microsoft.com/en-us/library/dd179355.aspx">REST
+                documentation</ulink> for detailed description of the service. You will need to be
+            familiar with basic concepts in order to use this service.
+        </para>
+    
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.features">
+        <title>Features</title>
+
+        <para>
+            <classname>Zend_Service_WindowsAzure</classname> provides the following functionality:
+        </para>
+
+        <itemizedlist>
+            <listitem>
+                <para>
+                    PHP classes for Windows Azure Blobs, Tables and Queues (for
+                    <acronym>CRUD</acronym> operations)
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    Helper Classes for HTTP transport, AuthN/AuthZ, REST and Error Management
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    Manageability, Instrumentation and Logging support
+                </para>
+            </listitem>
+        </itemizedlist>
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.architecture">
+        <title>Architecture</title>
+
+        <para>
+            <classname>Zend_Service_WindowsAzure</classname> provides access to Windows Azure's
+            storage, computation and management interfaces by abstracting the REST/XML interface
+            Windows Azure provides into a simple PHP API.
+        </para>
+
+        <para>
+            An application built using <classname>Zend_Service_WindowsAzure</classname> can access
+            Windows Azure's features, no matter if it is hosted on the Windows Azure platform or on
+            an in-premise web server.
+        </para>
+  </sect2>
+</sect1>

+ 359 - 0
documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Blob.xml

@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.service.windowsazure.storage.blob">
+    <title>Zend_Service_WindowsAzure_Storage_Blob</title>
+
+    <para>
+        Blob Storage stores sets of binary data. Blob storage offers the following three
+        resources: the storage account, containers, and blobs.  Within your storage account,
+        containers provide a way to organize sets of blobs within your storage account.
+    </para>
+
+    <para>
+        Blob Storage is offered by Windows Azure as a <acronym>REST</acronym> API which is
+        wrapped by the <classname>Zend_Service_WindowsAzure_Storage_Blob</classname> class in
+        order to provide a native PHP interface to the storage account.
+    </para>
+
+    <sect2 id="zend.service.windowsazure.storage.blob.api">
+        <title>API Examples</title>
+
+        <para>
+            This topic lists some examples of using the
+            <classname>Zend_Service_WindowsAzure_Storage_Blob</classname> class.  Other features are
+            available in the download package, as well as a detailed API documentation of those
+            features.
+        </para>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.create-container">
+            <title>Creating a storage container</title>
+
+            <para>
+                Using the following code, a blob storage container can be created on development
+                storage.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.create-container">
+                <title>Creating a storage container</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+$result = $storageClient->createContainer('testcontainer');
+
+echo 'Container name is: ' . $result->Name;
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.delete-container">
+            <title>Deleting a storage container</title>
+
+            <para>
+                Using the following code, a blob storage container can be removed from development
+                storage.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.delete-container">
+                <title>Deleting a storage container</title>
+
+                <programlisting role="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+$storageClient->deleteContainer('testcontainer');
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.storing-blob">
+            <title>Storing a blob</title>
+
+            <para>
+                Using the following code, a blob can be uploaded to a blob storage container on
+                development storage. Note that the container has already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.storing-blob">
+                <title>Storing a blob</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+
+// upload /home/maarten/example.txt to Azure
+$result = $storageClient->putBlob(
+    'testcontainer', 'example.txt', '/home/maarten/example.txt'
+);
+
+echo 'Blob name is: ' . $result->Name;
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.copy-blob">
+            <title>Copying a blob</title>
+
+            <para>
+                Using the following code, a blob can be copied from inside the storage account.  The
+                advantage of using this method is that the copy operation occurs in the Azure cloud
+                and does not involve downloading the blob. Note that the container has already been
+                created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.copy-blob">
+                <title>Copying a blob</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+
+// copy example.txt to example2.txt
+$result = $storageClient->copyBlob(
+    'testcontainer', 'example.txt', 'testcontainer', 'example2.txt'
+);
+
+echo 'Copied blob name is: ' . $result->Name;
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.download-blob">
+            <title>Downloading a blob</title>
+
+            <para>
+                Using the following code, a blob can be downloaded from a blob storage container on
+                development storage. Note that the container has already been created before and a
+                blob has been uploaded.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.download-blob">
+                <title>Downloading a blob</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+
+// download file to /home/maarten/example.txt
+$storageClient->getBlob(
+    'testcontainer', 'example.txt', '/home/maarten/example.txt'
+);
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.blob.api.public-blob">
+            <title>Making a blob publicly available</title>
+
+            <para>
+                By default, blob storage containers on Windows Azure are protected from public
+                viewing. If any user on the Internet should have access to a blob container, its ACL
+                can be set to public. Note that this applies to a complete container and not to a
+                single blob!
+            </para>
+
+            <para>
+                Using the following code, blob storage container ACL can be set on development
+                storage. Note that the container has already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.api.public-blob">
+                <title>Making a blob publicly available</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+
+// make container publicly available
+$storageClient->setContainerAcl('testcontainer', Zend_Service_WindowsAzure_Storage_Blob::ACL_PUBLIC);
+]]></programlisting>
+            </example>
+        </sect3>
+    </sect2>
+  
+    <sect2 id="zend.service.windowsazure.storage.blob.root">
+        <title>Root container</title>
+
+        <para>
+            Windows Azure Blob Storage provides support to work with a "root container". 
+            This means that a blob can be stored in the root of your storage account, 
+            i.e. <code>http://myaccount.blob.core.windows.net/somefile.txt</code>.
+        </para>
+        
+        <para>
+            In order to work with the root container, it should first be created using the
+            <methodname>createContainer()</methodname> method, naming the container
+            <varname>$root</varname>.  All other operations on the root container should be issued
+            with the container name set to <varname>$root</varname>.
+        </para>
+    </sect2>
+  
+    <sect2 id="zend.service.windowsazure.storage.blob.wrapper">
+        <title>Blob storage stream wrapper</title>
+
+        <para>
+            The Windows Azure SDK for PHP provides support for registering a blob storage
+            client as a PHP file stream wrapper. The blob storage stream wrapper provides
+            support for using regular file operations on Windows Azure Blob Storage.
+            For example, one can open a file from Windows Azure Blob Storage with
+            the <functionname>fopen()</functionname> function:
+        </para>
+
+        <example id="zend.service.windowsazure.storage.blob.wrapper.sample">
+            <title>Example usage of blob storage stream wrapper</title>
+
+            <programlisting language="php"><![CDATA[
+$fileHandle = fopen('azure://mycontainer/myfile.txt', 'r');
+
+// ...
+
+fclose($fileHandle);
+]]></programlisting>
+        </example>
+    
+        <para>
+            In order to do this, the Windows Azure SDK for PHP blob storage client
+            must be registered as a stream wrapper. This can be done by calling the
+            <methodname>registerStreamWrapper()</methodname> method:
+        </para>
+
+        <example id="zend.service.windowsazure.storage.blob.wrapper.register">
+            <title>Registering the blob storage stream wrapper</title>
+
+            <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob();
+$storageClient->registerStreamWrapper(); // registers azure:// on this storage client
+
+// or:
+
+$storageClient->registerStreamWrapper('blob://'); // regiters blob:// on this storage client
+]]></programlisting>
+        </example>
+    
+        <para>
+            To unregister the stream wrapper, the <methodname>unregisterStreamWrapper()</methodname>
+            method can be used.
+        </para>
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.storage.blob.sharedaccesssig">
+        <title>Shared Access Signature</title>
+
+        <para>
+            Windows Azure Bob Storage provides a feature called "Shared Access Signatures". By
+            default, there is only one level of authorization possible in Windows Azure Blob
+            Storage: either a container is private or it is public. Shared Access Signatures provide
+            a more granular method of authorization: read, write, delete and list permissions can be
+            assigned on a container or a blob and given to a specific client using an URL-based
+            model.
+        </para>
+        
+        <para>
+            An example would be the following signature:
+        </para>
+        
+        <literallayout>
+http://phpstorage.blob.core.windows.net/phpazuretestshared1?st=2009-08-17T09%3A06%3A17Z&amp;se=2009-08-17T09%3A56%3A17Z&amp;sr=c&amp;sp=w&amp;sig=hscQ7Su1nqd91OfMTwTkxabhJSaspx%2BD%2Fz8UqZAgn9s%3D
+        </literallayout>
+        
+        <para>
+            The above signature gives write access to the "phpazuretestshared1"
+            container of the "phpstorage" account.
+        </para>
+    
+        <sect3 id="zend.service.windowsazure.storage.blob.sharedaccesssig.generate">
+            <title>Generating a Shared Access Signature</title>
+
+            <para>
+                When you are the owner of a Windows Azure Bob Storage account, you can create and
+                distribute a shared access key for any type of resource in your account. To do this,
+                the <methodname>generateSharedAccessUrl()</methodname> method of the
+                <classname>Zend_Service_WindowsAzure_Storage_Blob</classname> storage client can be
+                used.
+            </para>
+            
+            <para>
+                The following example code will generate a Shared Access Signature for write access
+                in a container named "container1", within a timeframe of 3000 seconds.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.sharedaccesssig.generate-2">
+                <title>Generating a Shared Access Signature for a container</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient   = new Zend_Service_WindowsAzure_Storage_Blob();
+$sharedAccessUrl = storageClient->generateSharedAccessUrl(
+    'container1',
+    '',
+    'c', 
+    'w',
+    $storageClient ->isoDate(time() - 500),
+    $storageClient ->isoDate(time() + 3000)
+);
+]]></programlisting>
+            </example>
+            
+            <para>
+                The following example code will generate a Shared Access Signature for read access
+                in a blob named <filename>test.txt</filename> in a container named "container1"
+                within a time frame of 3000 seconds.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.sharedaccesssig-generate-3">
+                <title>Generating a Shared Access Signature for a blob</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient   = new Zend_Service_WindowsAzure_Storage_Blob();
+$sharedAccessUrl = storageClient->generateSharedAccessUrl(
+    'container1',
+    'test.txt',
+    'b', 
+    'r',
+    $storageClient ->isoDate(time() - 500),
+    $storageClient ->isoDate(time() + 3000)
+);
+]]></programlisting>
+            </example>
+        </sect3>
+        
+        <sect3 id="zend.service.windowsazure.storage.blob.sharedaccesssig.consume">
+            <title>Working with Shared Access Signatures from others</title>
+
+            <para>
+                When you receive a Shared Access Signature from someone else, you can use the
+                Windows Azure SDK for PHP to work with the addressed resource.  For example, the
+                following signature can be retrieved from the owner of a storage account:
+            </para>
+            
+            <literallayout>
+http://phpstorage.blob.core.windows.net/phpazuretestshared1?st=2009-08-17T09%3A06%3A17Z&amp;se=2009-08-17T09%3A56%3A17Z&amp;sr=c&amp;sp=w&amp;sig=hscQ7Su1nqd91OfMTwTkxabhJSaspx%2BD%2Fz8UqZAgn9s%3D
+            </literallayout>
+            
+            <para>
+                The above signature gives write access to the "phpazuretestshared1" "container" of
+                the phpstorage account. Since the shared key for the account is not known, the
+                Shared Access Signature can be used to work with the authorized resource.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.blob.sharedaccesssig.consuming">
+                <title>Consuming a Shared Access Signature for a container</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Blob(
+    'blob.core.windows.net', 'phpstorage', ''
+); 
+$storageClient->setCredentials( 
+    new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature() 
+);
+$storageClient->getCredentials()->setPermissionSet(array( 
+    'http://phpstorage.blob.core.windows.net/phpazuretestshared1?st=2009-08-17T09%3A06%3A17Z&se=2009-08-17T09%3A56%3A17Z&sr=c&sp=w&sig=hscQ7Su1nqd91OfMTwTkxabhJSaspx%2BD%2Fz8UqZAgn9s%3D' 
+));
+$storageClient->putBlob(
+    'phpazuretestshared1', 'NewBlob.txt', 'C:\Files\dataforazure.txt'
+);
+]]></programlisting>
+            </example>
+            
+            <para>
+                Note that there was no explicit permission to write to a specific blob. Instead, the
+                Windows Azure SDK for PHP determined that a permission was required to either write
+                to that specific blob, or to write to its container. Since only a signature was
+                available for the latter, the Windows Azure SDK for PHP chose those credentials to
+                perform the request on Windows Azure blob storage.
+            </para>
+        </sect3>
+    </sect2>
+</sect1>

+ 171 - 0
documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Queue.xml

@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.service.windowsazure.storage.queue">
+    <title>Zend_Service_WindowsAzure_Storage_Queue</title>
+
+    <para>
+        The Queue service stores messages that may be read by any client who has access to the
+        storage account.
+    </para>
+    
+    <para>
+        A queue can contain an unlimited number of messages, each of which can be up to 8 KB in
+        size. Messages are generally added to the end of the queue and retrieved from the front of
+        the queue, although first in/first out (<acronym>FIFO</acronym>) behavior is not guaranteed.
+        If you need to store messages larger than 8 KB, you can store message data as a queue or in
+        a table and then store a reference to the data as a message in a queue.
+    </para>
+
+    <para>
+        Queue Storage is offered by Windows Azure as a <acronym>REST</acronym> API which is wrapped
+        by the <classname>Zend_Service_WindowsAzure_Storage_Queue</classname> class in order to
+        provide a native PHP interface to the storage account.
+    </para>
+
+    <sect2 id="zend.service.windowsazure.storage.queue.api">
+        <title>API Examples</title>
+
+        <para>
+            This topic lists some examples of using the
+            <classname>Zend_Service_WindowsAzure_Storage_Queue</classname> class.  Other features
+            are available in the download package, as well as a detailed API documentation of those
+            features.
+        </para>
+
+        <sect3 id="zend.service.windowsazure.storage.queue.api.create-queue">
+            <title>Creating a queue</title>
+
+            <para>
+                Using the following code, a queue can be created on development storage.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.create-queue.example">
+                <title>Creating a queue</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+$result = $storageClient->createQueue('testqueue');
+
+echo 'Queue name is: ' . $result->Name;
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.queue.api.delete-queue">
+            <title>Deleting a queue</title>
+
+            <para>
+                Using the following code, a queue can be removed from development storage.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.delete-queue.example">
+                <title>Deleting a queue</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+$storageClient->deleteQueue('testqueue');
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.queue.api.storing-queue">
+            <title>Adding a message to a queue</title>
+
+            <para>
+                Using the following code, a message can be added to a queue on development storage.
+                Note that the queue has already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.storing-queue.example">
+                <title>Adding a message to a queue</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+
+// 3600 = time-to-live of the message, if omitted defaults to 7 days
+$storageClient->putMessage('testqueue', 'This is a test message', 3600); 
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.queue.api.read-queue">
+            <title>Reading a message from a queue</title>
+
+            <para>
+                Using the following code, a message can be read from a queue on development storage.
+                Note that the queue and message have already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.read-queue.example">
+                <title>Reading a message from a queue</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+
+// retrieve 10 messages at once
+$messages = $storageClient->getMessages('testqueue', 10);
+
+foreach ($messages as $message) {
+    echo $message->MessageText . "\r\n";
+}
+]]></programlisting>
+            </example>
+            
+            <para>
+                The messages that are read using <methodname>getMessages()</methodname> will be
+                invisible in the queue for 30 seconds, after which the messages will re-appear in
+                the queue.  To mark a message as processed and remove it from the queue, use the
+                <methodname>deleteMessage()</methodname> method.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.read-queue.processexample">
+                <title>Marking a message as processed</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+
+// retrieve 10 messages at once
+$messages = $storageClient->getMessages('testqueue', 10);
+
+foreach ($messages as $message) {
+    echo $message . "\r\n";
+
+    // Mark the message as processed
+    $storageClient->deleteMessage('testqueue', $message);
+}
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.queue.api.peek-queue">
+            <title>Check if there are messages in a queue</title>
+
+            <para>
+                Using the following code, a queue can be checked for new messages.  Note that the
+                queue and message have already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.queue.api.peek-queue.example">
+                <title>Check if there are messages in a queue</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Queue();
+
+// retrieve 10 messages at once
+$messages = $storageClient->peekMessages('testqueue', 10);
+
+foreach ($messages as $message) {
+    echo $message->MessageText . "\r\n";
+}
+]]></programlisting>
+            </example>
+            
+            <para>
+                Note that messages that are read using <methodname>peekMessages()</methodname> will
+                not become invisible in the queue, nor can they be marked as processed using the
+                <methodname>deleteMessage()</methodname> method.  To do this, use
+                <methodname>getMessages()</methodname> instead.
+            </para>
+        </sect3>
+    </sect2>
+</sect1>

+ 738 - 0
documentation/manual/en/module_specs/Zend_Service_WindowsAzure_Table.xml

@@ -0,0 +1,738 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.service.windowsazure.storage.table">
+    <title>Zend_Service_WindowsAzure_Storage_Table</title>
+
+    <para>
+        The Table service offers structured storage in the form of tables.
+    </para>
+
+    <para>
+        Table Storage is offered by Windows Azure as a REST API which is wrapped by the
+        <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class in order to provide a
+        native PHP interface to the storage account.
+    </para>
+
+    <para>
+        This topic lists some examples of using the
+        <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class.  Other features are
+        available in the download package, as well as a detailed API documentation of those
+        features.
+    </para>
+
+    <para>
+        Note that development table storage (in the Windows Azure SDK) does not support all features
+        provided by the API. Therefore, the examples listed on this page are to be used on Windows
+        Azure production table storage.
+    </para>
+
+    <sect2 id="zend.service.windowsazure.storage.table.api">
+        <title>Operations on tables</title>
+
+        <para>
+            This topic lists some samples of operations that can be executed on tables.
+        </para>
+
+        <sect3 id="zend.service.windowsazure.storage.table.api.create">
+            <title>Creating a table</title>
+
+            <para>
+                Using the following code, a table can be created on Windows Azure production table
+                storage.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.api.create.example">
+                <title>Creating a table</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$result = $storageClient->createTable('testtable');
+
+echo 'New table name is: ' . $result->Name;
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.table.api.list">
+            <title>Listing all tables</title>
+
+            <para>
+                Using the following code, a list of all tables in Windows Azure production table
+                storage can be queried.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.api.list.example">
+                <title>Listing all tables</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$result = $storageClient->listTables();
+foreach ($result as $table) {
+    echo 'Table name is: ' . $table->Name . "\r\n";
+}
+]]></programlisting>
+            </example>
+        </sect3>
+    </sect2>
+
+    <sect2 id="zend.service.windowsazure.storage.table.entities">
+        <title>Operations on entities</title>
+
+        <para>
+            Tables store data as collections of entities. Entities are similar to rows.  An entity
+            has a primary key and a set of properties. A property is a named, typed-value pair,
+            similar to a column.
+        </para>
+        
+        <para>
+            The Table service does not enforce any schema for tables, so two entities in the same
+            table may have different sets of properties. Developers may choose to enforce a schema
+            on the client side. A table may contain any number of entities.
+        </para>
+        
+        <para>
+            <classname>Zend_Service_WindowsAzure_Storage_Table</classname> provides 2 ways of
+            working with entities:
+        </para>
+
+        <itemizedlist>
+            <listitem>
+                <para>
+                    Enforced schema
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    No enforced schema
+                </para>
+            </listitem>
+        </itemizedlist>
+
+        <para>
+            All examples will make use of the following enforced schema class.
+        </para>
+
+        <example id="zend.service.windowsazure.storage.table.entities.schema">
+            <title>Enforced schema used in samples</title>
+
+            <programlisting language="php"><![CDATA[
+class SampleEntity extends Zend_Service_WindowsAzure_Storage_TableEntity
+{
+    /**
+    * @azure Name
+    */
+    public $Name;
+    
+    /**
+    * @azure Age Edm.Int64
+    */
+    public $Age;
+    
+    /**
+    * @azure Visible Edm.Boolean
+    */
+    public $Visible = false;
+}
+]]></programlisting>
+        </example>
+
+        <para>
+            Note that if no schema class is passed into table storage methods,
+            <classname>Zend_Service_WindowsAzure_Storage_Table</classname> automatically works with
+            <classname>Zend_Service_WindowsAzure_Storage_DynamicTableEntity</classname>.
+        </para>
+
+        <sect3 id="zend.service.windowsazure.storage.table.entities.enforced">
+            <title>Enforced schema entities</title>
+
+            <para>
+                To enforce a schema on the client side using the
+                <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class, you can create
+                a class which inherits
+                <classname>Zend_Service_WindowsAzure_Storage_TableEntity</classname>.  This class
+                provides some basic functionality for the
+                <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class to work with a
+                client-side schema.
+            </para>
+
+            <para>
+                Base properties provided by
+                <classname>Zend_Service_WindowsAzure_Storage_TableEntity</classname> are:
+            </para>
+
+            <itemizedlist>
+                <listitem>
+                    <para>
+                        PartitionKey (exposed through <methodname>getPartitionKey()</methodname> and
+                        <methodname>setPartitionKey()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        RowKey (exposed through <methodname>getRowKey()</methodname> and
+                        <methodname>setRowKey()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        Timestamp (exposed through <methodname>getTimestamp()</methodname> and
+                        <methodname>setTimestamp()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        Etag value (exposed through <methodname>getEtag()</methodname> and
+                        <methodname>setEtag()</methodname>)
+                    </para>
+                </listitem>
+            </itemizedlist>
+
+            <para>
+                Here's a sample class inheriting
+                <classname>Zend_Service_WindowsAzure_Storage_TableEntity</classname>:
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.enforced.schema">
+                <title>Sample enforced schema class</title>
+
+                <programlisting language="php"><![CDATA[
+class SampleEntity extends Zend_Service_WindowsAzure_Storage_TableEntity
+{
+    /**
+     * @azure Name
+     */
+    public $Name;
+    
+    /**
+     * @azure Age Edm.Int64
+     */
+    public $Age;
+    
+    /**
+     * @azure Visible Edm.Boolean
+     */
+    public $Visible = false;
+}
+]]></programlisting>
+            </example>
+
+            <para>
+                The <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class will map
+                any class inherited from
+                <classname>Zend_Service_WindowsAzure_Storage_TableEntity</classname> to Windows
+                Azure table storage entities with the correct data type and property name. All there
+                is to storing a property in Windows Azure is adding a docblock comment to a public
+                property or public getter/setter, in the following format:
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.enforced.schema-property">
+                <title>Enforced property</title>
+
+                <programlisting language="php"><![CDATA[
+/**
+ * @azure <property name in Windows Azure> <optional property type>
+ */
+public $<property name in PHP>;
+]]></programlisting>
+            </example>
+
+            <para>
+                Let's see how to define a propety "Age" as an integer on Windows Azure table
+                storage:
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.enforced.schema-property-sample">
+                <title>Sample enforced property</title>
+
+                <programlisting language="php"><![CDATA[
+/**
+ * @azure Age Edm.Int64
+ */
+public $Age;
+]]></programlisting>
+            </example>
+
+            <para>
+                Note that a property does not necessarily have to be named the same on Windows Azure
+                table storage. The Windows Azure table storage property name can be defined as well
+                as the type.
+            </para>
+
+            <para>
+                The following data types are supported:
+            </para>
+
+            <itemizedlist>
+                <listitem>
+                    <para>
+                        <constant>Edm.Binary</constant> - An array of bytes up to 64 KB in size.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.Boolean</constant> - A boolean value.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.DateTime</constant> - A 64-bit value expressed as Coordinated
+                        Universal Time (UTC). The supported DateTime range begins from 12:00
+                        midnight, January 1, 1601 A.D. (C.E.), Coordinated Universal Time (UTC). The
+                        range ends at December 31st, 9999.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.Double</constant> - A 64-bit floating point value.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.Guid</constant> - A 128-bit globally unique identifier.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.Int32</constant> - A 32-bit integer.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.Int64</constant> - A 64-bit integer.
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        <constant>Edm.String</constant> - A UTF-16-encoded value. String values may
+                        be up to 64 KB in size.
+                    </para>
+                </listitem>
+            </itemizedlist>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.table.entities.dynamic">
+            <title>No enforced schema entities (a.k.a. DynamicEntity)</title>
+
+            <para>
+                To use the <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class
+                without defining a schema, you can make use of the
+                <classname>Zend_Service_WindowsAzure_Storage_DynamicTableEntity</classname> class.
+                This class inherits <classname>Zend_Service_WindowsAzure_Storage_TableEntity</code>
+                like an enforced schema class does, but contains additional logic to make it dynamic
+                and not bound to a schema.
+            </para>
+
+            <para>
+                Base properties provided by
+                <classname>Zend_Service_WindowsAzure_Storage_DynamicTableEntity</classname> are:
+            </para>
+
+            <itemizedlist>
+                <listitem>
+                    <para>
+                        PartitionKey (exposed through <methodname>getPartitionKey()</methodname> and
+                        <methodname>setPartitionKey()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        RowKey (exposed through <methodname>getRowKey()</methodname> and
+                        <methodname>setRowKey()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        Timestamp (exposed through <methodname>getTimestamp()</methodname> and
+                        <methodname>setTimestamp()</methodname>)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        Etag value (exposed through <methodname>getEtag()</methodname> and
+                        <methodname>setEtag()</methodname>)
+                    </para>
+                </listitem>
+            </itemizedlist>
+
+            <para>
+                Other properties can be added on the fly. Their Windows Azure table storage type
+                will be determined on-the-fly:
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.dynamic.schema">
+                <title>Dynamicaly adding properties to Zend_Service_WindowsAzure_Storage_DynamicTableEntity</title>
+
+                <programlisting language="php"><![CDATA[
+$target = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity(
+    'partition1', '000001'
+);
+$target->Name = 'Name'; // Will add property "Name" of type "Edm.String"
+$target->Age  = 25;     // Will add property "Age" of type "Edm.Int32"
+]]></programlisting>
+            </example>
+
+            <para>
+                Optionally, a property type can be enforced:
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.dynamic.schema-forcedproperties">
+                <title>Forcing property types on Zend_Service_WindowsAzure_Storage_DynamicTableEntity</title>
+
+                <programlisting language="php"><![CDATA[
+$target = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity(
+    'partition1', '000001'
+);
+$target->Name = 'Name'; // Will add property "Name" of type "Edm.String"
+$target->Age  = 25;     // Will add property "Age" of type "Edm.Int32"
+
+// Change type of property "Age" to "Edm.Int32":
+$target->setAzurePropertyType('Age', 'Edm.Int64'); 
+]]></programlisting>
+            </example>
+
+            <para>
+                The <classname>Zend_Service_WindowsAzure_Storage_Table</classname> class
+                automatically works with
+                <classname>Zend_Service_WindowsAzure_Storage_TableEntity</classname> if no specific
+                class is passed into Table Storage methods.
+            </para>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.table.entities.api">
+            <title>Entities API examples</title>
+
+            <sect4 id="zend.service.windowsazure.storage.table.entities.api.insert">
+                <title>Inserting an entity</title>
+
+                <para>
+                    Using the following code, an entity can be inserted into a table named
+                    "testtable".  Note that the table has already been created before.
+                </para>
+
+                <example id="zend.service.windowsazure.storage.table.api.entities.insert.example">
+                    <title>Inserting an entity</title>
+
+                    <programlisting language="php"><![CDATA[
+$entity = new SampleEntity ('partition1', 'row1');
+$entity->FullName = "Maarten";
+$entity->Age = 25;
+$entity->Visible = true;
+
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$result = $storageClient->insertEntity('testtable', $entity);
+
+// Check the timestamp and etag of the newly inserted entity
+echo 'Timestamp: ' . $result->getTimestamp() . "\n";
+echo 'Etag: ' . $result->getEtag() . "\n";
+]]></programlisting>
+                </example>
+            </sect4>
+
+            <sect4 id="zend.service.windowsazure.storage.table.entities.api.retrieve-by-id">
+                <title>Retrieving an entity by partition key and row key</title>
+
+                <para>
+                    Using the following code, an entity can be retrieved by partition key and row
+                    key.  Note that the table and entity have already been created before.
+                </para>
+
+                <example id="zend.service.windowsazure.storage.table.entities.api.retrieve-by-id.example">
+                    <title>Retrieving an entity by partition key and row key</title>
+
+                    <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entity= $storageClient->retrieveEntityById(
+    'testtable', 'partition1', 'row1', 'SampleEntity'
+);
+]]></programlisting>
+                </example>
+            </sect4>
+
+            <sect4 id="zend.service.windowsazure.storage.table.entities.api.updating">
+                <title>Updating an entity</title>
+
+                <para>
+                    Using the following code, an entity can be updated. Note that the table and
+                    entity have already been created before.
+                </para>
+
+                <example id="zend.service.windowsazure.storage.table.api.entities.updating.example">
+                    <title>Updating an entity</title>
+
+                    <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entity = $storageClient->retrieveEntityById(
+    'testtable', 'partition1', 'row1', 'SampleEntity'
+);
+
+$entity->Name = 'New name';
+$result = $storageClient->updateEntity('testtable', $entity);
+]]></programlisting>
+                </example>
+
+                <para>
+                    If you want to make sure the entity has not been updated before, you can make
+                    sure the <acronym>Etag</acronym> of the entity is checked. If the entity already
+                    has had an update, the update will fail to make sure you do not overwrite any
+                    newer data.
+                </para>
+
+                <example id="zend.service.windowsazure.storage.table.entities.api.updating.example-etag">
+                    <title>Updating an entity (with Etag check)</title>
+
+                    <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entity = $storageClient->retrieveEntityById(
+    'testtable', 'partition1', 'row1', 'SampleEntity'
+);
+
+$entity->Name = 'New name';
+
+// last parameter instructs the Etag check:
+$result = $storageClient->updateEntity('testtable', $entity, true); 
+]]></programlisting>
+                </example>
+            </sect4>
+
+            <sect4 id="zend.service.windowsazure.storage.table.entities.api.delete">
+                <title>Deleting an entity</title>
+
+                <para>
+                    Using the following code, an entity can be deleted.  Note that the table and
+                    entity have already been created before.
+                </para>
+
+                <example id="zend.service.windowsazure.storage.table.entities.api.delete.example">
+                    <title>Deleting an entity</title>
+
+                    <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entity = $storageClient->retrieveEntityById(
+    'testtable', 'partition1', 'row1', 'SampleEntity'
+);
+$result = $storageClient->deleteEntity('testtable', $entity);
+]]></programlisting>
+                </example>
+            </sect4>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.table.entities.querying">
+            <title>Performing queries</title>
+
+            <para>
+                Queries in <classname>Zend_Service_WindowsAzure_Storage_Table</classname> table
+                storage can be performed in two ways:
+            </para>
+
+            <itemizedlist>
+                <listitem>
+                    <para>
+                        By manually creating a filter condition (involving learning a new query
+                        language)
+                    </para>
+                </listitem>
+
+                <listitem>
+                    <para>
+                        By using the fluent interface provided by the
+                        <classname>Zend_Service_WindowsAzure_Storage_Table</classname>
+                    </para>
+                </listitem>
+            </itemizedlist>
+
+            <para>
+                Using the following code, a table can be queried using a filter condition.  Note
+                that the table and entities have already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.entities.querying.query-filter">
+                <title>Performing queries using a filter condition</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entities = $storageClient->storageClient->retrieveEntities(
+    'testtable',
+    'Name eq \'Maarten\' and PartitionKey eq \'partition1\'',
+    'SampleEntity'
+);
+
+foreach ($entities as $entity) {
+    echo 'Name: ' . $entity->Name . "\n";
+}
+]]></programlisting>
+            </example>
+
+            <para>
+                Using the following code, a table can be queried using a fluent interface.  Note
+                that the table and entities have already been created before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.api.entities.query-fluent">
+                <title>Performing queries using a fluent interface</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+$entities = $storageClient->storageClient->retrieveEntities(
+    'testtable',
+    $storageClient->select()
+                  ->from($tableName)
+                  ->where('Name eq ?', 'Maarten')
+                  ->andWhere('PartitionKey eq ?', 'partition1'),
+    'SampleEntity'
+);
+
+foreach ($entities as $entity) {
+    echo 'Name: ' . $entity->Name . "\n";
+}
+]]></programlisting>
+            </example>
+        </sect3>
+
+        <sect3 id="zend.service.windowsazure.storage.table.entities.batch">
+            <title>Batch operations</title>
+
+            <para>
+                This topic demonstrates how to use the table entity group transaction features
+                provided by Windows Azure table storage. Windows Azure table storage supports batch
+                transactions on entities that are in the same table and belong to the same partition
+                group. A transaction can include at most 100 entities.
+            </para>
+
+            <para>
+                The following example uses a batch operation (transaction) to insert a set of
+                entities into the "testtable" table. Note that the table has already been created
+                before.
+            </para>
+
+            <example id="zend.service.windowsazure.storage.table.api.batch">
+                <title>Executing a batch operation</title>
+
+                <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+
+// Start batch
+$batch = $storageClient->startBatch();
+            
+// Insert entities in batch
+$entities = generateEntities();
+foreach ($entities as $entity) {
+    $storageClient->insertEntity($tableName, $entity);
+}
+            
+// Commit
+$batch->commit();
+]]></programlisting>
+            </example>
+        </sect3>
+    </sect2>
+  
+    <sect2 id="zend.service.windowsazure.storage.table.sessionhandler">
+        <title>Table storage session handler</title>
+
+        <para>
+            When running a PHP application on the Windows Azure platform in a load-balanced mode
+            (running 2 Web Role instances or more), it is important that PHP session data can be
+            shared between multiple Web Role instances. The Windows Azure SDK for PHP provides the
+            <classname>Zend_Service_WindowsAzure_SessionHandler</classname> class, which uses
+            Windows Azure Table Storage as a session handler for PHP applications.
+        </para>
+        
+        <para>
+            To use the <classname>Zend_Service_WindowsAzure_SessionHandler</classname> session
+            handler, it should be registered as the default session handler for your PHP
+            application:
+        </para>
+
+        <example id="zend.service.windowsazure.storage.table.api.sessionhandler-register">
+            <title>Registering table storage session handler</title>
+
+            <programlisting language="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+
+$sessionHandler = new Zend_Service_WindowsAzure_SessionHandler(
+    $storageClient , 'sessionstable'
+);
+$sessionHandler->register();
+]]></programlisting>
+        </example>
+    
+        <para>
+            The above classname registers the
+            <classname>Zend_Service_WindowsAzure_SessionHandler</classname> session handler and will
+            store sessions in a table called "sessionstable".
+        </para>
+        
+        <para>
+            After registration of the
+            <classname>Zend_Service_WindowsAzure_SessionHandler</classname> session handler,
+            sessions can be started and used in the same way as a normal PHP session:
+        </para>
+        
+        <example id="zend.service.windowsazure.storage.table.api.sessionhandler-usage">
+            <title>Using table storage session handler</title>
+
+            <programlisting role="php"><![CDATA[
+$storageClient = new Zend_Service_WindowsAzure_Storage_Table(
+    'table.core.windows.net', 'myaccount', 'myauthkey'
+);
+
+$sessionHandler = new Zend_Service_WindowsAzure_SessionHandler(
+    $storageClient , 'sessionstable'
+);
+$sessionHandler->register();
+
+session_start();
+
+if (!isset($_SESSION['firstVisit'])) {
+    $_SESSION['firstVisit'] = time();
+}
+
+// ...
+]]></programlisting>
+        </example>
+    
+        <warning>
+            <para>
+                The <classname>Zend_Service_WindowsAzure_SessionHandler</classname> session handler
+                should be registered before a call to <functionname>session_start()</functionname>
+                is made!
+            </para>
+        </warning>
+    </sect2>
+</sect1>

+ 184 - 0
library/Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php

@@ -0,0 +1,184 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: SharedKeyCredentials.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */ 
+abstract class Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+{
+	/**
+	 * Development storage account and key
+	 */
+	const DEVSTORE_ACCOUNT       = "devstoreaccount1";
+	const DEVSTORE_KEY           = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+	
+	/**
+	 * HTTP header prefixes
+	 */
+	const PREFIX_PROPERTIES      = "x-ms-prop-";
+	const PREFIX_METADATA        = "x-ms-meta-";
+	const PREFIX_STORAGE_HEADER  = "x-ms-";
+	
+	/**
+	 * Permissions
+	 */
+	const PERMISSION_READ        = "r";
+	const PERMISSION_WRITE       = "w";
+	const PERMISSION_DELETE      = "d";
+	const PERMISSION_LIST        = "l";
+
+	/**
+	 * Account name for Windows Azure
+	 *
+	 * @var string
+	 */
+	protected $_accountName = '';
+	
+	/**
+	 * Account key for Windows Azure
+	 *
+	 * @var string
+	 */
+	protected $_accountKey = '';
+	
+	/**
+	 * Use path-style URI's
+	 *
+	 * @var boolean
+	 */
+	protected $_usePathStyleUri = false;
+	
+	/**
+	 * Creates a new Zend_Service_WindowsAzure_Credentials_CredentialsAbstract instance
+	 *
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 */
+    public function __construct(
+        $accountName = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT, 
+        $accountKey = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY, 
+        $usePathStyleUri = false
+    ) {
+		$this->_accountName     = $accountName;
+		$this->_accountKey      = base64_decode($accountKey);
+		$this->_usePathStyleUri = $usePathStyleUri;
+	}
+	
+	/**
+	 * Set account name for Windows Azure
+	 *
+	 * @param  string $value
+     * @return Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+	 */
+	public function setAccountName($value = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT)
+	{
+		$this->_accountName = $value;
+        return $this;
+	}
+	
+	/**
+	 * Set account key for Windows Azure
+	 *
+	 * @param  string $value
+     * @return Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+	 */
+	public function setAccountkey($value = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY)
+	{
+		$this->_accountKey = base64_decode($value);
+        return $this;
+	}
+	
+	/**
+	 * Set use path-style URI's
+	 *
+	 * @param  boolean $value
+     * @return Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+	 */
+	public function setUsePathStyleUri($value = false)
+	{
+		$this->_usePathStyleUri = $value;
+        return $this;
+	}
+	
+	/**
+	 * Sign request URL with credentials
+	 *
+	 * @param string $requestUrl Request URL
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return string Signed request URL
+	 */
+	abstract public function signRequestUrl($requestUrl = '');
+	
+	/**
+	 * Sign request headers with credentials
+	 *
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param string $path Path for the request
+	 * @param string $queryString Query string for the request
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return array Array of headers
+	 */
+	abstract public function signRequestHeaders($httpVerb = Zend_Http_Client::GET, $path = '/', $queryString = '', $headers = null, $forTableStorage = false, $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);
+	
+	/**
+	 * Prepare query string for signing
+	 * 
+	 * @param  string $value Original query string
+	 * @return string        Query string for signing
+	 */
+	protected function _prepareQueryStringForSigning($value)
+	{
+	    // Check for 'comp='
+	    if (strpos($value, 'comp=') === false) {
+	        // If not found, no query string needed
+	        return '';
+	    } else {
+	        // If found, make sure it is the only parameter being used      
+    		if (strlen($value) > 0 && strpos($value, '?') === 0) {
+    			$value = substr($value, 1);
+    		}
+    		
+    		// Split parts
+    		$queryParts = explode('&', $value);
+    		foreach ($queryParts as $queryPart) {
+    		    if (strpos($queryPart, 'comp=') !== false) {
+    		        return '?' . $queryPart;
+    		    }
+    		}
+
+    		// Should never happen...
+			return '';
+	    }
+	}
+}

+ 306 - 0
library/Zend/Service/WindowsAzure/Credentials/SharedAccessSignature.php

@@ -0,0 +1,306 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: SharedKeyCredentials.php 24305 2009-07-23 06:30:04Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */ 
+class Zend_Service_WindowsAzure_Credentials_SharedAccessSignature
+    extends Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+{
+    /**
+     * Permission set
+     * 
+     * @var array
+     */
+    protected $_permissionSet = array();
+    
+	/**
+	 * Constructor
+	 *
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 * @param array $permissionSet Permission set
+	 */
+    public function __construct(
+        $accountName = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT, 
+        $accountKey = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY, 
+        $usePathStyleUri = false, 
+        $permissionSet = array()
+    ) {
+	    parent::__construct($accountName, $accountKey, $usePathStyleUri);
+	    $this->_permissionSet = $permissionSet;
+	}
+	
+	/**
+	 * Get permission set
+	 * 
+	 * @return array
+	 */
+    public function getPermissionSet()
+	{
+	    return $this->_permissionSet;   
+	}
+	
+	/**
+	 * Set permisison set
+	 * 
+	 * Warning: fine-grained permissions should be added prior to coarse-grained permissions.
+	 * For example: first add blob permissions, end with container-wide permissions.
+	 * 
+	 * Warning: the signed access signature URL must match the account name of the
+	 * Zend_Service_WindowsAzure_Credentials_SharedAccessSignature instance
+	 * 
+	 * @param array $value Permission set
+     * @return void
+	 */
+    public function setPermissionSet($value = array())
+	{
+		foreach ($value as $url) {
+			if (strpos($url, $this->_accountName) === false) {
+				throw new Zend_Service_WindowsAzure_Exception('The permission set can only contain URLs for the account name specified in the Zend_Service_WindowsAzure_SharedAccessSignatureCredentials instance.');
+			}
+		}
+	    $this->_permissionSet = $value;
+	}
+    
+    /**
+     * Create signature
+     * 
+     * @param string $path 		   Path for the request
+     * @param string $resource     Signed resource - container (c) - blob (b)
+     * @param string $permissions  Signed permissions - read (r), write (w), delete (d) and list (l)
+     * @param string $start        The time at which the Shared Access Signature becomes valid.
+     * @param string $expiry       The time at which the Shared Access Signature becomes invalid.
+     * @param string $identifier   Signed identifier
+     * @return string 
+     */
+    public function createSignature(
+        $path = '/', 
+        $resource = 'b', 
+        $permissions = 'r', 
+        $start = '', 
+        $expiry = '', 
+        $identifier = ''
+    ) {
+		// Determine path
+		if ($this->_usePathStyleUri) {
+			$path = substr($path, strpos($path, '/'));
+		}
+			
+		// Add trailing slash to $path
+		if (substr($path, 0, 1) !== '/') {
+		    $path = '/' . $path;
+		}
+
+		// Build canonicalized resource string
+		$canonicalizedResource  = '/' . $this->_accountName;
+		/*if ($this->_usePathStyleUri) {
+			$canonicalizedResource .= '/' . $this->_accountName;
+		}*/
+		$canonicalizedResource .= $path;
+		    
+		// Create string to sign   
+		$stringToSign   = array();
+		$stringToSign[] = $permissions;
+    	$stringToSign[] = $start;
+    	$stringToSign[] = $expiry;
+    	$stringToSign[] = $canonicalizedResource;
+    	$stringToSign[] = $identifier;
+
+    	$stringToSign = implode("\n", $stringToSign);
+    	$signature    = base64_encode(hash_hmac('sha256', $stringToSign, $this->_accountKey, true));
+	
+    	return $signature;
+    }
+
+    /**
+     * Create signed query string
+     * 
+     * @param string $path 		   Path for the request
+     * @param string $queryString  Query string for the request
+     * @param string $resource     Signed resource - container (c) - blob (b)
+     * @param string $permissions  Signed permissions - read (r), write (w), delete (d) and list (l)
+     * @param string $start        The time at which the Shared Access Signature becomes valid.
+     * @param string $expiry       The time at which the Shared Access Signature becomes invalid.
+     * @param string $identifier   Signed identifier
+     * @return string 
+     */
+    public function createSignedQueryString(
+        $path = '/', 
+        $queryString = '', 
+        $resource = 'b', 
+        $permissions = 'r', 
+        $start = '', 
+        $expiry = '', 
+        $identifier = ''
+    ) {
+        // Parts
+        $parts = array();
+        if ($start !== '') {
+            $parts[] = 'st=' . urlencode($start);
+        }
+        $parts[] = 'se=' . urlencode($expiry);
+        $parts[] = 'sr=' . $resource;
+        $parts[] = 'sp=' . $permissions;
+        if ($identifier !== '') {
+            $parts[] = 'si=' . urlencode($identifier);
+        }
+        $parts[] = 'sig=' . urlencode($this->createSignature($path, $resource, $permissions, $start, $expiry, $identifier));
+
+        // Assemble parts and query string
+        if ($queryString != '') {
+            $queryString .= '&';
+	    }
+        $queryString .= implode('&', $parts);
+
+        return $queryString;
+    }
+    
+    /**
+	 * Permission matches request?
+	 *
+	 * @param string $permissionUrl Permission URL
+	 * @param string $requestUrl Request URL
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return string Signed request URL
+	 */
+    public function permissionMatchesRequest(
+        $permissionUrl = '', 
+        $requestUrl = '', 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+        // Build requirements
+        $requiredResourceType = $resourceType;
+        if ($requiredResourceType == Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB) {
+            $requiredResourceType .= Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER;
+        }
+
+        // Parse permission url
+	    $parsedPermissionUrl = parse_url($permissionUrl);
+	    
+	    // Parse permission properties
+	    $permissionParts = explode('&', $parsedPermissionUrl['query']);
+	    
+	    // Parse request url
+	    $parsedRequestUrl = parse_url($requestUrl);
+	    
+	    // Check if permission matches request
+	    $matches = true;
+	    foreach ($permissionParts as $part) {
+	        list($property, $value) = explode('=', $part, 2);
+	        
+	        if ($property == 'sr') {
+	            $matches = $matches && (strpbrk($value, $requiredResourceType) !== false);
+	        }
+	        
+	    	if ($property == 'sp') {
+	            $matches = $matches && (strpbrk($value, $requiredPermission) !== false);
+	        }
+	    }
+	    
+	    // Ok, but... does the resource match?
+	    $matches = $matches && (strpos($parsedRequestUrl['path'], $parsedPermissionUrl['path']) !== false);
+	    
+        // Return
+	    return $matches;
+    }    
+    
+    /**
+	 * Sign request URL with credentials
+	 *
+	 * @param string $requestUrl Request URL
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return string Signed request URL
+	 */
+    public function signRequestUrl(
+        $requestUrl = '', 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+	    // Look for a matching permission
+	    foreach ($this->getPermissionSet() as $permittedUrl) {
+	        if ($this->permissionMatchesRequest($permittedUrl, $requestUrl, $resourceType, $requiredPermission)) {
+	            // This matches, append signature data
+	            $parsedPermittedUrl = parse_url($permittedUrl);
+
+	            if (strpos($requestUrl, '?') === false) {
+	                $requestUrl .= '?';
+	            } else {
+	                $requestUrl .= '&';
+	            }
+	            
+	            $requestUrl .= $parsedPermittedUrl['query'];
+
+	            // Return url
+	            return $requestUrl;
+	        }
+	    }
+	    
+	    // Return url, will be unsigned...
+	    return $requestUrl;
+	}
+    
+	/**
+	 * Sign request with credentials
+	 *
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param string $path Path for the request
+	 * @param string $queryString Query string for the request
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return array Array of headers
+	 */
+    public function signRequestHeaders(
+        $httpVerb = Zend_Http_Client::GET, 
+        $path = '/', 
+        $queryString = '', 
+        $headers = null, 
+        $forTableStorage = false, 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+	    return $headers;
+	}
+}

+ 154 - 0
library/Zend/Service/WindowsAzure/Credentials/SharedKey.php

@@ -0,0 +1,154 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: SharedKeyCredentials.php 35835 2009-12-17 09:40:36Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */ 
+class Zend_Service_WindowsAzure_Credentials_SharedKey 
+    extends Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+{
+    /**
+	 * Sign request URL with credentials
+	 *
+	 * @param string $requestUrl Request URL
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return string Signed request URL
+	 */
+    public function signRequestUrl(
+        $requestUrl = '', 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+	    return $requestUrl;
+	}
+	
+	/**
+	 * Sign request headers with credentials
+	 *
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param string $path Path for the request
+	 * @param string $queryString Query string for the request
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return array Array of headers
+	 */
+    public function signRequestHeaders(
+        $httpVerb = Zend_Http_Client::GET, 
+        $path = '/', 
+        $queryString = '', 
+        $headers = null, 
+        $forTableStorage = false, 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+		// http://github.com/sriramk/winazurestorage/blob/214010a2f8931bac9c96dfeb337d56fe084ca63b/winazurestorage.py
+
+		// Determine path
+		if ($this->_usePathStyleUri) {
+			$path = substr($path, strpos($path, '/'));
+		}
+
+		// Determine query
+		$queryString = $this->_prepareQueryStringForSigning($queryString);
+	
+		// Canonicalized headers
+		$canonicalizedHeaders = array();
+		
+		// Request date
+		$requestDate = '';
+		if (isset($headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'])) {
+		    $requestDate = $headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'];
+		} else {
+		    $requestDate = gmdate('D, d M Y H:i:s', time()) . ' GMT'; // RFC 1123
+		    $canonicalizedHeaders[] = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date:' . $requestDate;
+		}
+		
+		// Build canonicalized headers
+		if (!is_null($headers)) {
+			foreach ($headers as $header => $value) {
+				if (is_bool($value)) {
+					$value = $value === true ? 'True' : 'False';
+				}
+
+				$headers[$header] = $value;
+				if (substr($header, 0, strlen(Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER)) == Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER) {
+				    $canonicalizedHeaders[] = strtolower($header) . ':' . $value;
+				}
+			}
+		}
+		sort($canonicalizedHeaders);
+
+		// Build canonicalized resource string
+		$canonicalizedResource  = '/' . $this->_accountName;
+		if ($this->_usePathStyleUri) {
+			$canonicalizedResource .= '/' . $this->_accountName;
+		}
+		$canonicalizedResource .= $path;
+		if ($queryString !== '') {
+		    $canonicalizedResource .= $queryString;
+		}
+
+		// Create string to sign   
+		$stringToSign   = array();
+		$stringToSign[] = strtoupper($httpVerb); 	// VERB
+    	$stringToSign[] = "";						// Content-MD5
+    	$stringToSign[] = "";						// Content-Type
+    	$stringToSign[] = "";
+        // Date already in $canonicalizedHeaders
+    	// $stringToSign[] = self::PREFIX_STORAGE_HEADER . 'date:' . $requestDate; // Date
+    	
+    	if (!$forTableStorage && count($canonicalizedHeaders) > 0) {
+    		$stringToSign[] = implode("\n", $canonicalizedHeaders); // Canonicalized headers
+    	}
+    		
+    	$stringToSign[] = $canonicalizedResource;		 			// Canonicalized resource
+    	$stringToSign   = implode("\n", $stringToSign);
+    	$signString     = base64_encode(hash_hmac('sha256', $stringToSign, $this->_accountKey, true));
+
+    	// Sign request
+    	$headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'] = $requestDate;
+    	$headers['Authorization'] = 'SharedKey ' . $this->_accountName . ':' . $signString;
+    	
+    	// Return headers
+    	return $headers;
+	}
+}

+ 115 - 0
library/Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: SharedKeyCredentials.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */ 
+class Zend_Service_WindowsAzure_Credentials_SharedKeyLite 
+    extends Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+{
+    /**
+	 * Sign request URL with credentials
+	 *
+	 * @param string $requestUrl Request URL
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return string Signed request URL
+	 */
+	public function signRequestUrl($requestUrl = '', $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ)
+	{
+	    return $requestUrl;
+	}
+	
+	/**
+	 * Sign request headers with credentials
+	 *
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param string $path Path for the request
+	 * @param string $queryString Query string for the request
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return array Array of headers
+	 */
+    public function signRequestHeaders(
+        $httpVerb = Zend_Http_Client::GET, 
+        $path = '/', 
+        $queryString = '', 
+        $headers = null, 
+        $forTableStorage = false, 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
+    ) {
+		// Determine path
+		if ($this->_usePathStyleUri) {
+			$path = substr($path, strpos($path, '/'));
+		}
+
+		// Determine query
+		$queryString = $this->_prepareQueryStringForSigning($queryString);
+
+		// Build canonicalized resource string
+		$canonicalizedResource  = '/' . $this->_accountName;
+		if ($this->_usePathStyleUri) {
+			$canonicalizedResource .= '/' . $this->_accountName;
+		}
+		$canonicalizedResource .= $path;
+		if ($queryString !== '') {
+		    $canonicalizedResource .= $queryString;
+		}
+
+		// Request date
+		$requestDate = '';
+		if (isset($headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'])) {
+		    $requestDate = $headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'];
+		} else {
+		    $requestDate = gmdate('D, d M Y H:i:s', time()) . ' GMT'; // RFC 1123
+		}
+
+		// Create string to sign   
+		$stringToSign   = array();
+    	$stringToSign[] = $requestDate; // Date
+    	$stringToSign[] = $canonicalizedResource;		 			// Canonicalized resource
+    	$stringToSign   = implode("\n", $stringToSign);
+    	$signString     = base64_encode(hash_hmac('sha256', $stringToSign, $this->_accountKey, true));
+
+    	// Sign request
+    	$headers[Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PREFIX_STORAGE_HEADER . 'date'] = $requestDate;
+    	$headers['Authorization'] = 'SharedKeyLite ' . $this->_accountName . ':' . $signString;
+    	
+    	// Return headers
+    	return $headers;
+	}
+}

+ 35 - 0
library/Zend/Service/WindowsAzure/Exception.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Exception
+ * @version    $Id: Exception.php 28585 2009-09-07 12:12:56Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Service_Exception
+ */
+require_once 'Zend/Service/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Exception extends Zend_Service_Exception
+{}

+ 36 - 0
library/Zend/Service/WindowsAzure/RetryPolicy/Exception.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Exception
+ * @version    $Id: Exception.php 28585 2009-09-07 12:12:56Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_RetryPolicy_Exception extends Zend_Service_WindowsAzure_Exception
+{}

+ 58 - 0
library/Zend/Service/WindowsAzure/RetryPolicy/NoRetry.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @version    $Id: NoRetry.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_RetryPolicy_NoRetry extends Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+{
+    /**
+     * Execute function under retry policy
+     * 
+     * @param string|array $function       Function to execute
+     * @param array        $parameters     Parameters for function call
+     * @return mixed
+     */
+    public function execute($function, $parameters = array())
+    {
+        $returnValue = null;
+        
+        try
+        {
+            $returnValue = call_user_func_array($function, $parameters);
+            return $returnValue;
+        }
+        catch (Exception $ex)
+        {
+            throw $ex;
+        }
+    }
+}

+ 92 - 0
library/Zend/Service/WindowsAzure/RetryPolicy/RetryN.php

@@ -0,0 +1,92 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @version    $Id: RetryN.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_RetryPolicy_RetryN extends Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+{
+    /**
+     * Number of retries
+     * 
+     * @var int
+     */
+    protected $_retryCount = 1;
+    
+    /**
+     * Interval between retries (in milliseconds)
+     * 
+     * @var int
+     */
+    protected $_retryInterval = 0;
+    
+    /**
+     * Constructor
+     * 
+     * @param int $count                    Number of retries
+     * @param int $intervalBetweenRetries   Interval between retries (in milliseconds)
+     */
+    public function __construct($count = 1, $intervalBetweenRetries = 0)
+    {
+        $this->_retryCount = $count;
+        $this->_retryInterval = $intervalBetweenRetries;
+    }
+    
+    /**
+     * Execute function under retry policy
+     * 
+     * @param string|array $function       Function to execute
+     * @param array        $parameters     Parameters for function call
+     * @return mixed
+     */
+    public function execute($function, $parameters = array())
+    {
+        $returnValue = null;
+        
+        for ($retriesLeft = $this->_retryCount; $retriesLeft >= 0; --$retriesLeft) {
+            try {
+                $returnValue = call_user_func_array($function, $parameters);
+                return $returnValue;
+            } catch (Exception $ex) {
+                if ($retriesLeft == 1) {
+                    throw new Zend_Service_WindowsAzure_RetryPolicy_Exception("Exceeded retry count of " . $this->_retryCount . ". " . $ex->getMessage());
+                }
+                    
+                usleep($this->_retryInterval * 1000);
+            }
+        }
+    }
+}

+ 77 - 0
library/Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @version    $Id: RetryPolicy.php 28585 2009-09-07 12:12:56Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_NoRetry
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/NoRetry.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryN
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryN.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage RetryPolicy
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+{
+    /**
+     * Execute function under retry policy
+     * 
+     * @param string|array $function       Function to execute
+     * @param array        $parameters     Parameters for function call
+     * @return mixed
+     */
+    public abstract function execute($function, $parameters = array());
+    
+    /**
+     * Create a Zend_Service_WindowsAzure_RetryPolicy_NoRetry instance
+     * 
+     * @return Zend_Service_WindowsAzure_RetryPolicy_NoRetry
+     */
+    public static function noRetry()
+    {
+        return new Zend_Service_WindowsAzure_RetryPolicy_NoRetry();
+    }
+    
+    /**
+     * Create a Zend_Service_WindowsAzure_RetryPolicy_RetryN instance
+     * 
+     * @param int $count                    Number of retries
+     * @param int $intervalBetweenRetries   Interval between retries (in milliseconds)
+     * @return Zend_Service_WindowsAzure_RetryPolicy_RetryN
+     */
+    public static function retryN($count = 1, $intervalBetweenRetries = 0)
+    {
+        return new Zend_Service_WindowsAzure_RetryPolicy_RetryN($count, $intervalBetweenRetries);
+    }
+}

+ 217 - 0
library/Zend/Service/WindowsAzure/SessionHandler.php

@@ -0,0 +1,217 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Session
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Storage.php 21617 2009-06-12 10:46:31Z unknown $
+ */
+
+/** Zend_Service_WindowsAzure_Storage_Table */
+require_once 'Zend/Service/WindowsAzure/Storage/Table.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Session
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_SessionHandler
+{
+    /**
+     * Table storage
+     * 
+     * @var Zend_Service_WindowsAzure_Storage_Table
+     */
+    protected $_tableStorage;
+    
+    /**
+     * Session table name
+     * 
+     * @var string
+     */
+    protected $_sessionTable;
+    
+    /**
+     * Session table partition
+     * 
+     * @var string
+     */
+    protected $_sessionTablePartition;
+	
+    /**
+     * Creates a new Zend_Service_WindowsAzure_SessionHandler instance
+     * 
+     * @param Zend_Service_WindowsAzure_Storage_Table $tableStorage Table storage
+     * @param string $sessionTable Session table name
+     * @param string $sessionTablePartition Session table partition
+     */
+    public function __construct(Zend_Service_WindowsAzure_Storage_Table $tableStorage, $sessionTable = 'php-sessions', $sessionTablePartition = 'sessions')
+	{
+	    // Set properties
+		$this->_tableStorage = $tableStorage;
+		$this->_sessionTable = $sessionTable;
+		$this->_sessionTablePartition = $sessionTablePartition;
+	}
+	
+	/**
+	 * Registers the current session handler as PHP's session handler
+	 * 
+	 * @return boolean
+	 */
+	public function register()
+	{
+        return session_set_save_handler(array($this, 'open'),
+                                        array($this, 'close'),
+                                        array($this, 'read'),
+                                        array($this, 'write'),
+                                        array($this, 'destroy'),
+                                        array($this, 'gc')
+        );
+	}
+	
+    /**
+     * Open the session store
+     * 
+     * @return bool
+     */
+    public function open()
+    {
+    	// Make sure table exists
+    	$tableExists = $this->_tableStorage->tableExists($this->_sessionTable);
+    	if (!$tableExists) {
+		    $this->_tableStorage->createTable($this->_sessionTable);
+		}
+		
+		// Ok!
+		return true;
+    }
+
+    /**
+     * Close the session store
+     * 
+     * @return bool
+     */
+    public function close()
+    {
+        return true;
+    }
+    
+    /**
+     * Read a specific session
+     * 
+     * @param int $id Session Id
+     * @return string
+     */
+    public function read($id)
+    {
+        try
+        {
+            $sessionRecord = $this->_tableStorage->retrieveEntityById(
+                $this->_sessionTable,
+                $this->_sessionTablePartition,
+                $id
+            );
+            return base64_decode($sessionRecord->serializedData);
+        }
+        catch (Zend_Service_WindowsAzure_Exception $ex)
+        {
+            return '';
+        }
+    }
+    
+    /**
+     * Write a specific session
+     * 
+     * @param int $id Session Id
+     * @param string $serializedData Serialized PHP object
+     */
+    public function write($id, $serializedData)
+    {
+        $sessionRecord = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($this->_sessionTablePartition, $id);
+        $sessionRecord->sessionExpires = time();
+        $sessionRecord->serializedData = base64_encode($serializedData);
+        
+        $sessionRecord->setAzurePropertyType('sessionExpires', 'Edm.Int32');
+
+        try
+        {
+            $this->_tableStorage->updateEntity($this->_sessionTable, $sessionRecord);
+        }
+        catch (Zend_Service_WindowsAzure_Exception $unknownRecord)
+        {
+            $this->_tableStorage->insertEntity($this->_sessionTable, $sessionRecord);
+        }
+    }
+    
+    /**
+     * Destroy a specific session
+     * 
+     * @param int $id Session Id
+     * @return boolean
+     */
+    public function destroy($id)
+    {
+        try
+        {
+            $sessionRecord = $this->_tableStorage->retrieveEntityById(
+                $this->_sessionTable,
+                $this->_sessionTablePartition,
+                $id
+            );
+            $this->_tableStorage->deleteEntity($this->_sessionTable, $sessionRecord);
+            
+            return true;
+        }
+        catch (Zend_Service_WindowsAzure_Exception $ex)
+        {
+            return false;
+        }
+    }
+    
+    /**
+     * Garbage collector
+     * 
+     * @param int $lifeTime Session maximal lifetime
+     * @see session.gc_divisor  100
+     * @see session.gc_maxlifetime 1440
+     * @see session.gc_probability 1
+     * @usage Execution rate 1/100 (session.gc_probability/session.gc_divisor)
+     * @return boolean
+     */
+    public function gc($lifeTime)
+    {
+        try
+        {
+            $result = $this->_tableStorage->retrieveEntities($this->_sessionTable, 'PartitionKey eq \'' . $this->_sessionTablePartition . '\' and sessionExpires lt ' . (time() - $lifeTime));
+            foreach ($result as $sessionRecord)
+            {
+                $this->_tableStorage->deleteEntity($this->_sessionTable, $sessionRecord);
+            }
+            return true;
+        }
+        catch (Zend_Service_WindowsAzure_exception $ex)
+        {
+            return false;
+        }
+    }
+}

+ 457 - 0
library/Zend/Service/WindowsAzure/Storage.php

@@ -0,0 +1,457 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Storage.php 35835 2009-12-17 09:40:36Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_SharedKey
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKey.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @see Zend_Http_Response
+ */
+require_once 'Zend/Http/Response.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage
+{
+	/**
+	 * Development storage URLS
+	 */
+	const URL_DEV_BLOB      = "127.0.0.1:10000";
+	const URL_DEV_QUEUE     = "127.0.0.1:10001";
+	const URL_DEV_TABLE     = "127.0.0.1:10002";
+	
+	/**
+	 * Live storage URLS
+	 */
+	const URL_CLOUD_BLOB    = "blob.core.windows.net";
+	const URL_CLOUD_QUEUE   = "queue.core.windows.net";
+	const URL_CLOUD_TABLE   = "table.core.windows.net";
+	
+	/**
+	 * Resource types
+	 */
+	const RESOURCE_UNKNOWN     = "unknown";
+	const RESOURCE_CONTAINER   = "c";
+	const RESOURCE_BLOB        = "b";
+	const RESOURCE_TABLE       = "t";
+	const RESOURCE_ENTITY      = "e";
+	const RESOURCE_QUEUE       = "q";
+	
+	/**
+	 * Current API version
+	 * 
+	 * @var string
+	 */
+	protected $_apiVersion = '2009-04-14';
+	
+	/**
+	 * Storage host name
+	 *
+	 * @var string
+	 */
+	protected $_host = '';
+	
+	/**
+	 * Account name for Windows Azure
+	 *
+	 * @var string
+	 */
+	protected $_accountName = '';
+	
+	/**
+	 * Account key for Windows Azure
+	 *
+	 * @var string
+	 */
+	protected $_accountKey = '';
+	
+	/**
+	 * Use path-style URI's
+	 *
+	 * @var boolean
+	 */
+	protected $_usePathStyleUri = false;
+	
+	/**
+	 * Zend_Service_WindowsAzure_Credentials_CredentialsAbstract instance
+	 *
+	 * @var Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+	 */
+	protected $_credentials = null;
+	
+	/**
+	 * Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract instance
+	 * 
+	 * @var Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+	 */
+	protected $_retryPolicy = null;
+	
+	/**
+	 * Zend_Http_Client channel used for communication with REST services
+	 * 
+	 * @var Zend_Http_Client
+	 */
+	protected $_httpClientChannel = null;
+	
+	/**
+	 * Use proxy?
+	 * 
+	 * @var boolean
+	 */
+	protected $_useProxy = false;
+	
+	/**
+	 * Proxy url
+	 * 
+	 * @var string
+	 */
+	protected $_proxyUrl = '';
+	
+	/**
+	 * Proxy port
+	 * 
+	 * @var int
+	 */
+	protected $_proxyPort = 80;
+	
+	/**
+	 * Proxy credentials
+	 * 
+	 * @var string
+	 */
+	protected $_proxyCredentials = '';
+	
+	/**
+	 * Creates a new Zend_Service_WindowsAzure_Storage instance
+	 *
+	 * @param string $host Storage host name
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
+	 */
+    public function __construct(
+        $host = self::URL_DEV_BLOB, 
+        $accountName = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT, 
+        $accountKey = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY, 
+        $usePathStyleUri = false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null
+    ) {
+		$this->_host            = $host;
+		$this->_accountName     = $accountName;
+		$this->_accountKey      = $accountKey;
+		$this->_usePathStyleUri = $usePathStyleUri;
+		
+		// Using local storage?
+        if (!$this->_usePathStyleUri 
+            && ($this->_host == self::URL_DEV_BLOB 
+                || $this->_host == self::URL_DEV_QUEUE 
+                || $this->_host == self::URL_DEV_TABLE)
+        ) {
+			// Local storage
+			$this->_usePathStyleUri = true;
+		}
+		
+		if (is_null($this->_credentials)) {
+            $this->_credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey(
+                $this->_accountName, $this->_accountKey, $this->_usePathStyleUri
+            );
+		}
+		
+		$this->_retryPolicy = $retryPolicy;
+		if (is_null($this->_retryPolicy)) {
+		    $this->_retryPolicy = Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::noRetry();
+		}
+		
+		// Setup default Zend_Http_Client channel
+		$this->_httpClientChannel = new Zend_Http_Client(
+			null,
+			array(
+				'adapter' => 'Zend_Http_Client_Adapter_Proxy',
+				'curloptions' => array(
+					CURLOPT_FOLLOWLOCATION => true,
+					CURLOPT_TIMEOUT => 120,
+				)
+			)
+		);
+	}
+	
+	/**
+	 * Set the HTTP client channel to use
+	 * 
+	 * @param Zend_Http_Client_Adapter_Interface|string $adapterInstance Adapter instance or adapter class name.
+	 */
+	public function setHttpClientChannel($adapterInstance = 'Zend_Http_Client_Adapter_Proxy')
+	{
+		$this->_httpClientChannel->setAdapter($adapterInstance);
+	}
+	
+	/**
+	 * Set retry policy to use when making requests
+	 *
+	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
+	 */
+	public function setRetryPolicy(Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null)
+	{
+		$this->_retryPolicy = $retryPolicy;
+		if (is_null($this->_retryPolicy)) {
+		    $this->_retryPolicy = Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::noRetry();
+		}
+	}
+	
+	/**
+	 * Set proxy
+	 * 
+	 * @param boolean $useProxy         Use proxy?
+	 * @param string  $proxyUrl         Proxy URL
+	 * @param int     $proxyPort        Proxy port
+	 * @param string  $proxyCredentials Proxy credentials
+	 */
+	public function setProxy($useProxy = false, $proxyUrl = '', $proxyPort = 80, $proxyCredentials = '')
+	{
+	    $this->_useProxy         = $useProxy;
+	    $this->_proxyUrl         = $proxyUrl;
+	    $this->_proxyPort        = $proxyPort;
+	    $this->_proxyCredentials = $proxyCredentials;
+	    
+	    if ($this->_useProxy) {
+	    	$credentials = explode(':', $this->_proxyCredentials);
+	    	
+	    	$this->_httpClientChannel->setConfig(array(
+				'proxy_host' => $this->_proxyUrl,
+	    		'proxy_port' => $this->_proxyPort,
+	    		'proxy_user' => $credentials[0],
+	    		'proxy_pass' => $credentials[1],
+	    	));
+	    } else {
+			$this->_httpClientChannel->setConfig(array(
+				'proxy_host' => '',
+	    		'proxy_port' => 8080,
+	    		'proxy_user' => '',
+	    		'proxy_pass' => '',
+	    	));
+	    }
+	}
+	
+	/**
+	 * Returns the Windows Azure account name
+	 * 
+	 * @return string
+	 */
+	public function getAccountName()
+	{
+		return $this->_accountName;
+	}
+	
+	/**
+	 * Get base URL for creating requests
+	 *
+	 * @return string
+	 */
+	public function getBaseUrl()
+	{
+        if ($this->_usePathStyleUri) {
+			return 'http://' . $this->_host . '/' . $this->_accountName;
+        }
+
+        return 'http://' . $this->_accountName . '.' . $this->_host;
+	}
+	
+	/**
+	 * Set Zend_Service_WindowsAzure_Credentials_CredentialsAbstract instance
+	 * 
+	 * @param Zend_Service_WindowsAzure_Credentials_CredentialsAbstract $credentials Zend_Service_WindowsAzure_Credentials_CredentialsAbstract instance to use for request signing.
+	 */
+	public function setCredentials(Zend_Service_WindowsAzure_Credentials_CredentialsAbstract $credentials)
+	{
+	    $this->_credentials = $credentials;
+	    $this->_credentials->setAccountName($this->_accountName);
+	    $this->_credentials->setAccountkey($this->_accountKey);
+	    $this->_credentials->setUsePathStyleUri($this->_usePathStyleUri);
+	}
+	
+	/**
+	 * Get Zend_Service_WindowsAzure_Credentials_CredentialsAbstract instance
+	 * 
+	 * @return Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+	 */
+	public function getCredentials()
+	{
+	    return $this->_credentials;
+	}
+	
+	/**
+	 * Perform request using Zend_Http_Client channel
+	 *
+	 * @param string $path Path
+	 * @param string $queryString Query string
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param mixed $rawData Optional RAW HTTP data to be sent over the wire
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return Zend_Http_Response
+	 */
+    protected function _performRequest(
+        $path = '/', 
+        $queryString = '', 
+        $httpVerb = Zend_Http_Client::GET, 
+        $headers = array(), 
+        $forTableStorage = false, 
+        $rawData = null, 
+        $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, 
+        $requiredPermission = Zend_Service_WindowsAzure_Credentials::PERMISSION_READ
+    ) {
+	    // Clean path
+		if (strpos($path, '/') !== 0) {
+			$path = '/' . $path;
+		}
+			
+		// Clean headers
+		if (is_null($headers)) {
+		    $headers = array();
+		}
+		
+		// Ensure cUrl will also work correctly:
+		//  - disable Content-Type if required
+		//  - disable Expect: 100 Continue
+		if (!isset($headers["Content-Type"])) {
+			$headers["Content-Type"] = '';
+		}
+		$headers["Expect"]= '';
+
+		// Add version header
+		$headers['x-ms-version'] = $this->_apiVersion;
+		    
+		// URL encoding
+		$path           = self::urlencode($path);
+		$queryString    = self::urlencode($queryString);
+
+		// Generate URL and sign request
+        $requestUrl     = $this->_credentials
+                        ->signRequestUrl($this->getBaseUrl() . $path . $queryString, $resourceType, $requiredPermission);
+        $requestHeaders = $this->_credentials
+                        ->signRequestHeaders($httpVerb, $path, $queryString, $headers, $forTableStorage, $resourceType, $requiredPermission);
+
+		// Prepare request
+		$this->_httpClientChannel->resetParameters(true);
+		$this->_httpClientChannel->setUri($requestUrl);
+		$this->_httpClientChannel->setHeaders($requestHeaders);
+		$this->_httpClientChannel->setRawData($rawData);
+		
+		// Execute request
+		$response = $this->_retryPolicy->execute(
+		    array($this->_httpClientChannel, 'request'),
+		    array($httpVerb)
+		);
+		
+		return $response;
+	}
+	
+	/** 
+	 * Parse result from Zend_Http_Response
+	 *
+	 * @param Zend_Http_Response $response Response from HTTP call
+	 * @return object
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	protected function _parseResponse(Zend_Http_Response $response = null)
+	{
+		if (is_null($response)) {
+			throw new Zend_Service_WindowsAzure_Exception('Response should not be null.');
+		}
+		
+        $xml = @simplexml_load_string($response->getBody());
+        
+        if ($xml !== false) {
+            // Fetch all namespaces 
+            $namespaces = array_merge($xml->getNamespaces(true), $xml->getDocNamespaces(true)); 
+            
+            // Register all namespace prefixes
+            foreach ($namespaces as $prefix => $ns) { 
+                if ($prefix != '') {
+                    $xml->registerXPathNamespace($prefix, $ns);
+                } 
+            } 
+        }
+        
+        return $xml;
+	}
+	
+	/**
+	 * Generate ISO 8601 compliant date string in UTC time zone
+	 * 
+	 * @param int $timestamp
+	 * @return string
+	 */
+	public function isoDate($timestamp = null) 
+	{        
+	    $tz = @date_default_timezone_get();
+	    @date_default_timezone_set('UTC');
+	    
+	    if (is_null($timestamp)) {
+	        $timestamp = time();
+	    }
+	        
+	    $returnValue = str_replace('+00:00', '.0000000Z', @date('c', $timestamp));
+	    @date_default_timezone_set($tz);
+	    return $returnValue;
+	}
+	
+	/**
+	 * URL encode function
+	 * 
+	 * @param  string $value Value to encode
+	 * @return string        Encoded value
+	 */
+	public static function urlencode($value)
+	{
+	    return str_replace(' ', '%20', $value);
+	}
+}

+ 248 - 0
library/Zend/Service/WindowsAzure/Storage/Batch.php

@@ -0,0 +1,248 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Storage.php 21617 2009-06-12 10:46:31Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_Batch
+{	
+    /**
+     * Storage client the batch is defined on
+     * 
+     * @var Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
+     */
+    protected $_storageClient = null;
+    
+    /**
+     * For table storage?
+     * 
+     * @var boolean
+     */
+    protected $_forTableStorage = false;
+    
+    /**
+     * Base URL
+     * 
+     * @var string
+     */
+    protected $_baseUrl;
+    
+    /**
+     * Pending operations
+     * 
+     * @var unknown_type
+     */
+    protected $_operations = array();
+    
+    /**
+     * Does the batch contain a single select?
+     * 
+     * @var boolean
+     */
+    protected $_isSingleSelect = false;
+    
+    /**
+     * Creates a new Zend_Service_WindowsAzure_Storage_Batch
+     * 
+     * @param Zend_Service_WindowsAzure_Storage_BatchStorageAbstract $storageClient Storage client the batch is defined on
+     */
+    public function __construct(Zend_Service_WindowsAzure_Storage_BatchStorageAbstract $storageClient = null, $baseUrl = '')
+    {
+        $this->_storageClient = $storageClient;
+        $this->_baseUrl       = $baseUrl;
+        $this->_beginBatch();
+    }
+    
+	/**
+	 * Get base URL for creating requests
+	 *
+	 * @return string
+	 */
+	public function getBaseUrl()
+	{
+		return $this->_baseUrl;
+	}
+    
+    /**
+     * Starts a new batch operation set
+     * 
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    protected function _beginBatch()
+    {
+        $this->_storageClient->setCurrentBatch($this);
+    }
+    
+    /**
+     * Cleanup current batch
+     */
+    protected function _clean()
+    {
+        unset($this->_operations);
+        $this->_storageClient->setCurrentBatch(null);
+        $this->_storageClient = null;
+        unset($this);
+    }
+
+	/**
+	 * Enlist operation in current batch
+	 *
+	 * @param string $path Path
+	 * @param string $queryString Query string
+	 * @param string $httpVerb HTTP verb the request will use
+	 * @param array $headers x-ms headers to add
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param mixed $rawData Optional RAW HTTP data to be sent over the wire
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function enlistOperation($path = '/', $queryString = '', $httpVerb = Zend_Http_Client::GET, $headers = array(), $forTableStorage = false, $rawData = null)
+	{
+	    // Set _forTableStorage
+	    if ($forTableStorage) {
+	        $this->_forTableStorage = true;
+	    }
+	    
+	    // Set _isSingleSelect
+	    if ($httpVerb == Zend_Http_Client::GET) {
+	        if (count($this->_operations) > 0) {
+	            throw new Zend_Service_WindowsAzure_Exception("Select operations can only be performed in an empty batch transaction.");
+	        }
+	        $this->_isSingleSelect = true;
+	    }
+	    
+	    // Clean path
+		if (strpos($path, '/') !== 0) {
+			$path = '/' . $path;
+		}
+			
+		// Clean headers
+		if (is_null($headers)) {
+		    $headers = array();
+		}
+		    
+		// URL encoding
+		$path           = Zend_Service_WindowsAzure_Storage::urlencode($path);
+		$queryString    = Zend_Service_WindowsAzure_Storage::urlencode($queryString);
+
+		// Generate URL
+		$requestUrl     = $this->getBaseUrl() . $path . $queryString;
+		
+		// Generate $rawData
+		if (is_null($rawData)) {
+		    $rawData = '';
+		}
+		    
+		// Add headers
+		if ($httpVerb != Zend_Http_Client::GET) {
+    		$headers['Content-ID'] = count($this->_operations) + 1;
+    		if ($httpVerb != Zend_Http_Client::DELETE) {
+    		    $headers['Content-Type'] = 'application/atom+xml;type=entry';
+    		}
+    		$headers['Content-Length'] = strlen($rawData);
+		}
+		    
+		// Generate $operation
+		$operation = '';
+		$operation .= $httpVerb . ' ' . $requestUrl . ' HTTP/1.1' . "\n";
+		foreach ($headers as $key => $value)
+		{
+		    $operation .= $key . ': ' . $value . "\n";
+		}
+		$operation .= "\n";
+		
+		// Add data
+		$operation .= $rawData;
+
+		// Store operation
+		$this->_operations[] = $operation;	        
+	}
+    
+    /**
+     * Commit current batch
+     * 
+     * @return Zend_Http_Response
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    public function commit()
+    {
+        // Perform batch
+        $response = $this->_storageClient->performBatch($this->_operations, $this->_forTableStorage, $this->_isSingleSelect);
+        
+        // Dispose
+        $this->_clean();
+        
+        // Parse response
+        $errors = null;
+        preg_match_all('/<message (.*)>(.*)<\/message>/', $response->getBody(), $errors);
+        
+        // Error?
+        if (count($errors[2]) > 0) {
+            throw new Zend_Service_WindowsAzure_Exception('An error has occured while committing a batch: ' . $errors[2][0]);
+        }
+        
+        // Return
+        return $response;
+    }
+    
+    /**
+     * Rollback current batch
+     */
+    public function rollback()
+    {
+        // Dispose
+        $this->_clean();
+    }
+    
+    /**
+     * Get operation count
+     * 
+     * @return integer
+     */
+    public function getOperationCount()
+    {
+        return count($this->_operations);
+    }
+    
+    /**
+     * Is single select?
+     * 
+     * @return boolean
+     */
+    public function isSingleSelect()
+    {
+        return $this->_isSingleSelect;
+    }
+}

+ 193 - 0
library/Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php

@@ -0,0 +1,193 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Storage.php 21617 2009-06-12 10:46:31Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_Batch
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/Batch.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @see Zend_Http_Response
+ */
+require_once 'Zend/Http/Response.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Service_WindowsAzure_Storage_BatchStorageAbstract 
+    extends Zend_Service_WindowsAzure_Storage
+{	
+    /**
+     * Current batch
+     * 
+     * @var Zend_Service_WindowsAzure_Storage_Batch
+     */
+    protected $_currentBatch = null;
+    
+    /**
+     * Set current batch
+     * 
+     * @param Zend_Service_WindowsAzure_Storage_Batch $batch Current batch
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    public function setCurrentBatch(Zend_Service_WindowsAzure_Storage_Batch $batch = null)
+    {
+        if (!is_null($batch) && $this->isInBatch()) {
+            throw new Zend_Service_WindowsAzure_Exception('Only one batch can be active at a time.');
+        }
+        $this->_currentBatch = $batch;
+    }
+    
+    /**
+     * Get current batch
+     * 
+     * @return Zend_Service_WindowsAzure_Storage_Batch
+     */
+    public function getCurrentBatch()
+    {
+        return $this->_currentBatch;
+    }
+    
+    /**
+     * Is there a current batch?
+     * 
+     * @return boolean
+     */
+    public function isInBatch()
+    {
+        return !is_null($this->_currentBatch);
+    }
+    
+    /**
+     * Starts a new batch operation set
+     * 
+     * @return Zend_Service_WindowsAzure_Storage_Batch
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    public function startBatch()
+    {
+        return new Zend_Service_WindowsAzure_Storage_Batch($this, $this->getBaseUrl());
+    }
+	
+	/**
+	 * Perform batch using Zend_Http_Client channel, combining all batch operations into one request
+	 *
+	 * @param array $operations Operations in batch
+	 * @param boolean $forTableStorage Is the request for table storage?
+	 * @param boolean $isSingleSelect Is the request a single select statement?
+	 * @param string $resourceType Resource type
+	 * @param string $requiredPermission Required permission
+	 * @return Zend_Http_Response
+	 */
+	public function performBatch($operations = array(), $forTableStorage = false, $isSingleSelect = false, $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN, $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ)
+	{
+	    // Generate boundaries
+	    $batchBoundary = 'batch_' . md5(time() . microtime());
+	    $changesetBoundary = 'changeset_' . md5(time() . microtime());
+	    
+	    // Set headers
+	    $headers = array();
+	    
+		// Add version header
+		$headers['x-ms-version'] = $this->_apiVersion;
+		
+		// Add content-type header
+		$headers['Content-Type'] = 'multipart/mixed; boundary=' . $batchBoundary;
+
+		// Set path and query string
+		$path           = '/$batch';
+		$queryString    = '';
+		
+		// Set verb
+		$httpVerb = Zend_Http_Client::POST;
+		
+		// Generate raw data
+    	$rawData = '';
+    		
+		// Single select?
+		if ($isSingleSelect) {
+		    $operation = $operations[0];
+		    $rawData .= '--' . $batchBoundary . "\n";
+            $rawData .= 'Content-Type: application/http' . "\n";
+            $rawData .= 'Content-Transfer-Encoding: binary' . "\n\n";
+            $rawData .= $operation; 
+            $rawData .= '--' . $batchBoundary . '--';
+		} else {
+    		$rawData .= '--' . $batchBoundary . "\n";
+    		$rawData .= 'Content-Type: multipart/mixed; boundary=' . $changesetBoundary . "\n\n";
+    		
+        		// Add operations
+        		foreach ($operations as $operation)
+        		{
+                    $rawData .= '--' . $changesetBoundary . "\n";
+                	$rawData .= 'Content-Type: application/http' . "\n";
+                	$rawData .= 'Content-Transfer-Encoding: binary' . "\n\n";
+                	$rawData .= $operation;
+        		}
+        		$rawData .= '--' . $changesetBoundary . '--' . "\n";
+    		    		    
+    		$rawData .= '--' . $batchBoundary . '--';
+		}
+
+		// Generate URL and sign request
+		$requestUrl     = $this->_credentials->signRequestUrl($this->getBaseUrl() . $path . $queryString, $resourceType, $requiredPermission);
+		$requestHeaders = $this->_credentials->signRequestHeaders($httpVerb, $path, $queryString, $headers, $forTableStorage, $resourceType, $requiredPermission);
+
+		// Prepare request
+		$this->_httpClientChannel->resetParameters(true);
+		$this->_httpClientChannel->setUri($requestUrl);
+		$this->_httpClientChannel->setHeaders($requestHeaders);
+		$this->_httpClientChannel->setRawData($rawData);
+		
+		// Execute request
+		$response = $this->_retryPolicy->execute(
+		    array($this->_httpClientChannel, 'request'),
+		    array($httpVerb)
+		);
+
+		return $response;
+	}
+}

+ 1423 - 0
library/Zend/Service/WindowsAzure/Storage/Blob.php

@@ -0,0 +1,1423 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://todo     name_todo
+ * @version    $Id: Blob.php 35835 2009-12-17 09:40:36Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_SharedKey
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKey.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_SharedAccessSignature
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedAccessSignature.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @see Zend_Http_Response
+ */
+require_once 'Zend/Http/Response.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_BlobContainer
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/BlobContainer.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_BlobInstance
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/BlobInstance.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_SignedIdentifier
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/SignedIdentifier.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_Blob extends Zend_Service_WindowsAzure_Storage
+{
+	/**
+	 * ACL - Private access
+	 */
+	const ACL_PRIVATE = false;
+	
+	/**
+	 * ACL - Public access
+	 */
+	const ACL_PUBLIC = true;
+	
+	/**
+	 * Maximal blob size (in bytes)
+	 */
+	const MAX_BLOB_SIZE = 67108864;
+
+	/**
+	 * Maximal blob transfer size (in bytes)
+	 */
+	const MAX_BLOB_TRANSFER_SIZE = 4194304;
+	
+    /**
+     * Stream wrapper clients
+     *
+     * @var array
+     */
+    protected static $_wrapperClients = array();
+    
+    /**
+     * SharedAccessSignature credentials
+     * 
+     * @var Zend_Service_WindowsAzure_Credentials_SharedAccessSignature
+     */
+    private $_sharedAccessSignatureCredentials = null;
+	
+	/**
+	 * Creates a new Zend_Service_WindowsAzure_Storage_Blob instance
+	 *
+	 * @param string $host Storage host name
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
+	 */
+	public function __construct($host = Zend_Service_WindowsAzure_Storage::URL_DEV_BLOB, $accountName = Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, $accountKey = Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, $usePathStyleUri = false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null)
+	{
+		parent::__construct($host, $accountName, $accountKey, $usePathStyleUri, $retryPolicy);
+		
+		// API version
+		$this->_apiVersion = '2009-07-17';
+		
+		// SharedAccessSignature credentials
+		$this->_sharedAccessSignatureCredentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature($accountName, $accountKey, $usePathStyleUri);
+	}
+	
+	/**
+	 * Check if a blob exists
+	 * 
+	 * @param string $containerName Container name
+	 * @param string $blobName      Blob name
+	 * @return boolean
+	 */
+	public function blobExists($containerName = '', $blobName = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		
+		// List blobs
+        $blobs = $this->listBlobs($containerName, $blobName, '', 1);
+        foreach ($blobs as $blob) {
+            if ($blob->Name == $blobName) {
+                return true;
+            }
+        }
+        
+        return false;
+	}
+	
+	/**
+	 * Check if a container exists
+	 * 
+	 * @param string $containerName Container name
+	 * @return boolean
+	 */
+	public function containerExists($containerName = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+			
+		// List containers
+        $containers = $this->listContainers($containerName, 1);
+        foreach ($containers as $container) {
+            if ($container->Name == $containerName) {
+                return true;
+            }
+        }
+        
+        return false;
+	}
+	
+	/**
+	 * Create container
+	 *
+	 * @param string $containerName Container name
+	 * @param array  $metadata      Key/value pairs of meta data
+	 * @return object Container properties
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function createContainer($containerName = '', $metadata = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if (!is_array($metadata)) {
+			throw new Zend_Service_WindowsAzure_Exception('Meta data should be an array of key and value pairs.');
+		}
+			
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container', Zend_Http_Client::PUT, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);			
+		if ($response->isSuccessful()) {
+		    return new Zend_Service_WindowsAzure_Storage_BlobContainer(
+		        $containerName,
+		        $response->getHeader('Etag'),
+		        $response->getHeader('Last-modified'),
+		        $metadata
+		    );
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get container ACL
+	 *
+	 * @param string $containerName Container name
+	 * @param bool   $signedIdentifiers Display only public/private or display signed identifiers?
+	 * @return bool Acl, to be compared with Zend_Service_WindowsAzure_Storage_Blob::ACL_*
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getContainerAcl($containerName = '', $signedIdentifiers = false)
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container&comp=acl', Zend_Http_Client::GET, array(), false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);
+		if ($response->isSuccessful()) {
+		    if ($signedIdentifiers == false)  {
+		        // Only public/private
+			    return $response->getHeader('x-ms-prop-publicaccess') == 'True';
+		    } else {
+       		    // Parse result
+    		    $result = $this->_parseResponse($response);
+    		    if (!$result) {
+    		        return array();
+    		    }
+    
+    		    $entries = null;
+    		    if ($result->SignedIdentifier) {
+        		    if (count($result->SignedIdentifier) > 1) {
+        		        $entries = $result->SignedIdentifier;
+        		    } else {
+        		        $entries = array($result->SignedIdentifier);
+        		    }
+    		    }
+    		    
+    		    // Return value
+    		    $returnValue = array();
+    		    foreach ($entries as $entry) {
+    		        $returnValue[] = new Zend_Service_WindowsAzure_Storage_SignedIdentifier(
+    		            $entry->Id,
+    		            $entry->AccessPolicy ? $entry->AccessPolicy->Start ? $entry->AccessPolicy->Start : '' : '',
+    		            $entry->AccessPolicy ? $entry->AccessPolicy->Expiry ? $entry->AccessPolicy->Expiry : '' : '',
+    		            $entry->AccessPolicy ? $entry->AccessPolicy->Permission ? $entry->AccessPolicy->Permission : '' : ''
+    		        );
+    		    }
+    		    
+    		    // Return
+    		    return $returnValue;
+		    }			   
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Set container ACL
+	 *
+	 * @param string $containerName Container name
+	 * @param bool $acl Zend_Service_WindowsAzure_Storage_Blob::ACL_*
+	 * @param array $signedIdentifiers Signed identifiers
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function setContainerAcl($containerName = '', $acl = self::ACL_PRIVATE, $signedIdentifiers = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+
+		// Policies
+		$policies = null;
+		if (is_array($signedIdentifiers) && count($signedIdentifiers) > 0) {
+		    $policies  = '';
+		    $policies .= '<?xml version="1.0" encoding="utf-8"?>' . "\r\n";
+		    $policies .= '<SignedIdentifiers>' . "\r\n";
+		    foreach ($signedIdentifiers as $signedIdentifier) {
+		        $policies .= '  <SignedIdentifier>' . "\r\n";
+		        $policies .= '    <Id>' . $signedIdentifier->Id . '</Id>' . "\r\n";
+		        $policies .= '    <AccessPolicy>' . "\r\n";
+		        if ($signedIdentifier->Start != '')
+		            $policies .= '      <Start>' . $signedIdentifier->Start . '</Start>' . "\r\n";
+		        if ($signedIdentifier->Expiry != '')
+		            $policies .= '      <Expiry>' . $signedIdentifier->Expiry . '</Expiry>' . "\r\n";
+		        if ($signedIdentifier->Permissions != '')
+		            $policies .= '      <Permission>' . $signedIdentifier->Permissions . '</Permission>' . "\r\n";
+		        $policies .= '    </AccessPolicy>' . "\r\n";
+		        $policies .= '  </SignedIdentifier>' . "\r\n";
+		    }
+		    $policies .= '</SignedIdentifiers>' . "\r\n";
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container&comp=acl', Zend_Http_Client::PUT, array('x-ms-prop-publicaccess' => $acl), false, $policies, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get container
+	 * 
+	 * @param string $containerName  Container name
+	 * @return Zend_Service_WindowsAzure_Storage_BlobContainer
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getContainer($containerName = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		    
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container', Zend_Http_Client::GET, array(), false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);	
+		if ($response->isSuccessful()) {
+		    // Parse metadata
+		    $metadata = array();
+		    foreach ($response->getHeaders() as $key => $value) {
+		        if (substr(strtolower($key), 0, 10) == "x-ms-meta-") {
+		            $metadata[str_replace("x-ms-meta-", '', strtolower($key))] = $value;
+		        }
+		    }
+
+		    // Return container
+		    return new Zend_Service_WindowsAzure_Storage_BlobContainer(
+		        $containerName,
+		        $response->getHeader('Etag'),
+		        $response->getHeader('Last-modified'),
+		        $metadata
+		    );
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get container metadata
+	 * 
+	 * @param string $containerName  Container name
+	 * @return array Key/value pairs of meta data
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getContainerMetadata($containerName = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		
+	    return $this->getContainer($containerName)->Metadata;
+	}
+	
+	/**
+	 * Set container metadata
+	 * 
+	 * Calling the Set Container Metadata operation overwrites all existing metadata that is associated with the container. It's not possible to modify an individual name/value pair.
+	 *
+	 * @param string $containerName      Container name
+	 * @param array  $metadata           Key/value pairs of meta data
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function setContainerMetadata($containerName = '', $metadata = array(), $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if (!is_array($metadata)) {
+			throw new Zend_Service_WindowsAzure_Exception('Meta data should be an array of key and value pairs.');
+		}
+		if (count($metadata) == 0) {
+		    return;
+		}
+		    
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Additional headers?
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container&comp=metadata', Zend_Http_Client::PUT, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Delete container
+	 *
+	 * @param string $containerName      Container name
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteContainer($containerName = '', $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+			
+		// Additional headers?
+		$headers = array();
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($containerName, '?restype=container', Zend_Http_Client::DELETE, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * List containers
+	 *
+	 * @param string $prefix     Optional. Filters the results to return only containers whose name begins with the specified prefix.
+	 * @param int    $maxResults Optional. Specifies the maximum number of containers to return per call to Azure storage. This does NOT affect list size returned by this function. (maximum: 5000)
+	 * @param string $marker     Optional string value that identifies the portion of the list to be returned with the next list operation.
+	 * @param int    $currentResultCount Current result count (internal use)
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function listContainers($prefix = null, $maxResults = null, $marker = null, $currentResultCount = 0)
+	{
+	    // Build query string
+	    $queryString = '?comp=list';
+	    if (!is_null($prefix)) {
+	        $queryString .= '&prefix=' . $prefix;
+	    }
+	    if (!is_null($maxResults)) {
+	        $queryString .= '&maxresults=' . $maxResults;
+	    }
+	    if (!is_null($marker)) {
+	        $queryString .= '&marker=' . $marker;
+	    }
+	        
+		// Perform request
+		$response = $this->_performRequest('', $queryString, Zend_Http_Client::GET, array(), false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_CONTAINER, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_LIST);	
+		if ($response->isSuccessful()) {
+			$xmlContainers = $this->_parseResponse($response)->Containers->Container;
+			$xmlMarker = (string) $this->_parseResponse($response)->NextMarker;
+
+			$containers = array();
+			if (!is_null($xmlContainers)) {
+				for ($i = 0; $i < count($xmlContainers); $i++) {
+					$containers[] = new Zend_Service_WindowsAzure_Storage_BlobContainer(
+						(string)$xmlContainers[$i]->Name,
+						(string)$xmlContainers[$i]->Etag,
+						(string)$xmlContainers[$i]->LastModified
+					);
+				}
+			}
+			$currentResultCount = $currentResultCount + count($containers);
+			if (!is_null($maxResults) && $currentResultCount < $maxResults) {
+    			if (!is_null($xmlMarker) && $xmlMarker != '') {
+    			    $containers = array_merge($containers, $this->listContainers($prefix, $maxResults, $xmlMarker, $currentResultCount));
+    			}
+			}
+			if (!is_null($maxResults) && count($containers) > $maxResults) {
+			    $containers = array_slice($containers, 0, $maxResults);
+			}
+			    
+			return $containers;
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Put blob
+	 *
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param string $localFileName      Local file name to be uploaded
+	 * @param array  $metadata           Key/value pairs of meta data
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @return object Partial blob properties
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function putBlob($containerName = '', $blobName = '', $localFileName = '', $metadata = array(), $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($localFileName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Local file name is not specified.');
+		}
+		if (!file_exists($localFileName)) {
+			throw new Zend_Service_WindowsAzure_Exception('Local file not found.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+			
+		// Check file size
+		if (filesize($localFileName) >= self::MAX_BLOB_SIZE) {
+			return $this->putLargeBlob($containerName, $blobName, $localFileName, $metadata);
+		}
+
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Additional headers?
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// File contents
+		$fileContents = file_get_contents($localFileName);
+		
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		
+		// Perform request
+		$response = $this->_performRequest($resourceName, '', Zend_Http_Client::PUT, $headers, false, $fileContents, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);	
+		if ($response->isSuccessful()) {
+			return new Zend_Service_WindowsAzure_Storage_BlobInstance(
+				$containerName,
+				$blobName,
+				$response->getHeader('Etag'),
+				$response->getHeader('Last-modified'),
+				$this->getBaseUrl() . '/' . $containerName . '/' . $blobName,
+				strlen($fileContents),
+				'',
+				'',
+				'',
+				false,
+		        $metadata
+			);
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Put large blob (> 64 MB)
+	 *
+	 * @param string $containerName Container name
+	 * @param string $blobName Blob name
+	 * @param string $localFileName Local file name to be uploaded
+	 * @param array  $metadata      Key/value pairs of meta data
+	 * @return object Partial blob properties
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function putLargeBlob($containerName = '', $blobName = '', $localFileName = '', $metadata = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($localFileName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Local file name is not specified.');
+		}
+		if (!file_exists($localFileName)) {
+			throw new Zend_Service_WindowsAzure_Exception('Local file not found.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+			
+		// Check file size
+		if (filesize($localFileName) < self::MAX_BLOB_SIZE) {
+			return $this->putBlob($containerName, $blobName, $localFileName, $metadata);
+		}
+			
+		// Determine number of parts
+		$numberOfParts = ceil( filesize($localFileName) / self::MAX_BLOB_TRANSFER_SIZE );
+		
+		// Generate block id's
+		$blockIdentifiers = array();
+		for ($i = 0; $i < $numberOfParts; $i++) {
+			$blockIdentifiers[] = $this->_generateBlockId($i);
+		}
+		
+		// Open file
+		$fp = fopen($localFileName, 'r');
+		if ($fp === false) {
+			throw new Zend_Service_WindowsAzure_Exception('Could not open local file.');
+		}
+			
+		// Upload parts
+		for ($i = 0; $i < $numberOfParts; $i++) {
+			// Seek position in file
+			fseek($fp, $i * self::MAX_BLOB_TRANSFER_SIZE);
+			
+			// Read contents
+			$fileContents = fread($fp, self::MAX_BLOB_TRANSFER_SIZE);
+			
+			// Put block
+			$this->putBlock($containerName, $blobName, $blockIdentifiers[$i], $fileContents);
+			
+			// Dispose file contents
+			$fileContents = null;
+			unset($fileContents);
+		}
+		
+		// Close file
+		fclose($fp);
+		
+		// Put block list
+		$this->putBlockList($containerName, $blobName, $blockIdentifiers, $metadata);
+		
+		// Return information of the blob
+		return $this->getBlobInstance($containerName, $blobName);
+	}			
+			
+	/**
+	 * Put large blob block
+	 *
+	 * @param string $containerName Container name
+	 * @param string $blobName      Blob name
+	 * @param string $identifier    Block ID
+	 * @param array  $contents      Contents of the block
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function putBlock($containerName = '', $blobName = '', $identifier = '', $contents = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($identifier === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Block identifier is not specified.');
+		}
+		if (strlen($contents) > self::MAX_BLOB_TRANSFER_SIZE) {
+			throw new Zend_Service_WindowsAzure_Exception('Block size is too big.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+			
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		
+    	// Upload
+		$response = $this->_performRequest($resourceName, '?comp=block&blockid=' . base64_encode($identifier), Zend_Http_Client::PUT, null, false, $contents, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Put block list
+	 *
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param array $blockList           Array of block identifiers
+	 * @param array  $metadata           Key/value pairs of meta data
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function putBlockList($containerName = '', $blobName = '', $blockList = array(), $metadata = array(), $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if (count($blockList) == 0) {
+			throw new Zend_Service_WindowsAzure_Exception('Block list does not contain any elements.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+		
+		// Generate block list
+		$blocks = '';
+		foreach ($blockList as $block) {
+			$blocks .= '  <Latest>' . base64_encode($block) . '</Latest>' . "\n";
+		}
+		
+		// Generate block list request
+		$fileContents = utf8_encode(implode("\n", array(
+			'<?xml version="1.0" encoding="utf-8"?>',
+			'<BlockList>',
+			$blocks,
+			'</BlockList>'
+		)));
+		
+	    // Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Additional headers?
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		
+		// Perform request
+		$response = $this->_performRequest($resourceName, '?comp=blocklist', Zend_Http_Client::PUT, $headers, false, $fileContents, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get block list
+	 *
+	 * @param string $containerName Container name
+	 * @param string $blobName      Blob name
+	 * @param integer $type         Type of block list to retrieve. 0 = all, 1 = committed, 2 = uncommitted
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getBlockList($containerName = '', $blobName = '', $type = 0)
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($type < 0 || $type > 2) {
+			throw new Zend_Service_WindowsAzure_Exception('Invalid type of block list to retrieve.');
+		}
+			
+		// Set $blockListType
+		$blockListType = 'all';
+		if ($type == 1) {
+		    $blockListType = 'committed';
+		}
+		if ($type == 2) {
+		    $blockListType = 'uncommitted';
+		}
+		    
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+			
+		// Perform request
+		$response = $this->_performRequest($resourceName, '?comp=blocklist&blocklisttype=' . $blockListType, Zend_Http_Client::GET, array(), false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);
+		if ($response->isSuccessful()) {
+		    // Parse response
+		    $blockList = $this->_parseResponse($response);
+		    
+		    // Create return value
+		    $returnValue = array();
+		    if ($blockList->CommittedBlocks) {
+			    foreach ($blockList->CommittedBlocks->Block as $block) {
+			        $returnValue['CommittedBlocks'][] = (object)array(
+			            'Name' => (string)$block->Name,
+			            'Size' => (string)$block->Size
+			        );
+			    }
+		    }
+		    if ($blockList->UncommittedBlocks)  {
+			    foreach ($blockList->UncommittedBlocks->Block as $block) {
+			        $returnValue['UncommittedBlocks'][] = (object)array(
+			            'Name' => (string)$block->Name,
+			            'Size' => (string)$block->Size
+			        );
+			    }
+		    }
+		    
+		    return $returnValue;
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+			
+	/**
+	 * Copy blob
+	 *
+	 * @param string $sourceContainerName       Source container name
+	 * @param string $sourceBlobName            Source blob name
+	 * @param string $destinationContainerName  Destination container name
+	 * @param string $destinationBlobName       Destination blob name
+	 * @param array  $metadata                  Key/value pairs of meta data
+	 * @param array  $additionalHeaders         Additional headers. See http://msdn.microsoft.com/en-us/library/dd894037.aspx for more information.
+	 * @return object Partial blob properties
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function copyBlob($sourceContainerName = '', $sourceBlobName = '', $destinationContainerName = '', $destinationBlobName = '', $metadata = array(), $additionalHeaders = array())
+	{
+		if ($sourceContainerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Source container name is not specified.');
+		}
+		if (!self::isValidContainerName($sourceContainerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Source container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($sourceBlobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Source blob name is not specified.');
+		}
+		if ($destinationContainerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Destination container name is not specified.');
+		}
+		if (!self::isValidContainerName($destinationContainerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Destination container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($destinationBlobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Destination blob name is not specified.');
+		}
+		if ($sourceContainerName === '$root' && strpos($sourceBlobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+		if ($destinationContainerName === '$root' && strpos($destinationBlobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Additional headers?
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Resource names
+		$sourceResourceName = self::createResourceName($sourceContainerName, $sourceBlobName);
+		$destinationResourceName = self::createResourceName($destinationContainerName, $destinationBlobName);
+		
+		// Set source blob
+		$headers["x-ms-copy-source"] = '/' . $this->_accountName . '/' . $sourceResourceName;
+
+		// Perform request
+		$response = $this->_performRequest($destinationResourceName, '', Zend_Http_Client::PUT, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if ($response->isSuccessful()) {
+			return new Zend_Service_WindowsAzure_Storage_BlobInstance(
+				$destinationContainerName,
+				$destinationBlobName,
+				$response->getHeader('Etag'),
+				$response->getHeader('Last-modified'),
+				$this->getBaseUrl() . '/' . $destinationContainerName . '/' . $destinationBlobName,
+				0,
+				'',
+				'',
+				'',
+				false,
+		        $metadata
+			);
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get blob
+	 *
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param string $localFileName      Local file name to store downloaded blob
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getBlob($containerName = '', $blobName = '', $localFileName = '', $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($localFileName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Local file name is not specified.');
+		}
+			
+		// Additional headers?
+		$headers = array();
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		
+		// Perform request
+		$response = $this->_performRequest($resourceName, '', Zend_Http_Client::GET, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);
+		if ($response->isSuccessful()) {
+			file_put_contents($localFileName, $response->getBody());
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get container
+	 * 
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @return Zend_Service_WindowsAzure_Storage_BlobInstance
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getBlobInstance($containerName = '', $blobName = '', $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+	        
+		// Additional headers?
+		$headers = array();
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+        // Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		    
+		// Perform request
+		$response = $this->_performRequest($resourceName, '', Zend_Http_Client::HEAD, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ);
+		if ($response->isSuccessful()) {
+		    // Parse metadata
+		    $metadata = array();
+		    foreach ($response->getHeaders() as $key => $value) {
+		        if (substr(strtolower($key), 0, 10) == "x-ms-meta-") {
+		            $metadata[str_replace("x-ms-meta-", '', strtolower($key))] = $value;
+		        }
+		    }
+
+		    // Return blob
+			return new Zend_Service_WindowsAzure_Storage_BlobInstance(
+				$containerName,
+				$blobName,
+				$response->getHeader('Etag'),
+				$response->getHeader('Last-modified'),
+				$this->getBaseUrl() . '/' . $containerName . '/' . $blobName,
+				$response->getHeader('Content-Length'),
+				$response->getHeader('Content-Type'),
+				$response->getHeader('Content-Encoding'),
+				$response->getHeader('Content-Language'),
+				false,
+		        $metadata
+			);
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get blob metadata
+	 * 
+	 * @param string $containerName  Container name
+	 * @param string $blobName Blob name
+	 * @return array Key/value pairs of meta data
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getBlobMetadata($containerName = '', $blobName = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+		
+	    return $this->getBlobInstance($containerName, $blobName)->Metadata;
+	}
+	
+	/**
+	 * Set blob metadata
+	 * 
+	 * Calling the Set Blob Metadata operation overwrites all existing metadata that is associated with the blob. It's not possible to modify an individual name/value pair.
+	 *
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param array  $metadata           Key/value pairs of meta data
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function setBlobMetadata($containerName = '', $blobName = '', $metadata = array(), $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+		if (count($metadata) == 0) {
+		    return;
+		}
+		    
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Additional headers?
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($containerName . '/' . $blobName, '?comp=metadata', Zend_Http_Client::PUT, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Delete blob
+	 *
+	 * @param string $containerName      Container name
+	 * @param string $blobName           Blob name
+	 * @param array  $additionalHeaders  Additional headers. See http://msdn.microsoft.com/en-us/library/dd179371.aspx for more information.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteBlob($containerName = '', $blobName = '', $additionalHeaders = array())
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+		if ($blobName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Blob name is not specified.');
+		}
+		if ($containerName === '$root' && strpos($blobName, '/') !== false) {
+		    throw new Zend_Service_WindowsAzure_Exception('Blobs stored in the root container can not have a name containing a forward slash (/).');
+		}
+			
+		// Additional headers?
+		$headers = array();
+		foreach ($additionalHeaders as $key => $value) {
+		    $headers[$key] = $value;
+		}
+		
+		// Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+		
+		// Perform request
+		$response = $this->_performRequest($resourceName, '', Zend_Http_Client::DELETE, $headers, false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_WRITE);
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * List blobs
+	 *
+	 * @param string $containerName Container name
+	 * @param string $prefix     Optional. Filters the results to return only blobs whose name begins with the specified prefix.
+	 * @param string $delimiter  Optional. Delimiter, i.e. '/', for specifying folder hierarchy
+	 * @param int    $maxResults Optional. Specifies the maximum number of blobs to return per call to Azure storage. This does NOT affect list size returned by this function. (maximum: 5000)
+	 * @param string $marker     Optional string value that identifies the portion of the list to be returned with the next list operation.
+	 * @param int    $currentResultCount Current result count (internal use)
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function listBlobs($containerName = '', $prefix = '', $delimiter = '', $maxResults = null, $marker = null, $currentResultCount = 0)
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+			
+	    // Build query string
+	    $queryString = '?restype=container&comp=list';
+        if (!is_null($prefix)) {
+	        $queryString .= '&prefix=' . $prefix;
+        }
+		if ($delimiter !== '') {
+			$queryString .= '&delimiter=' . $delimiter;
+		}
+	    if (!is_null($maxResults)) {
+	        $queryString .= '&maxresults=' . $maxResults;
+	    }
+	    if (!is_null($marker)) {
+	        $queryString .= '&marker=' . $marker;
+	    }
+
+	    // Perform request
+		$response = $this->_performRequest($containerName, $queryString, Zend_Http_Client::GET, array(), false, null, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB, Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_LIST);
+		if ($response->isSuccessful()) {
+		    // Return value
+		    $blobs = array();
+		    
+			// Blobs
+			$xmlBlobs = $this->_parseResponse($response)->Blobs->Blob;
+			if (!is_null($xmlBlobs)) {
+				for ($i = 0; $i < count($xmlBlobs); $i++) {
+					$blobs[] = new Zend_Service_WindowsAzure_Storage_BlobInstance(
+						$containerName,
+						(string)$xmlBlobs[$i]->Name,
+						(string)$xmlBlobs[$i]->Etag,
+						(string)$xmlBlobs[$i]->LastModified,
+						(string)$xmlBlobs[$i]->Url,
+						(string)$xmlBlobs[$i]->Size,
+						(string)$xmlBlobs[$i]->ContentType,
+						(string)$xmlBlobs[$i]->ContentEncoding,
+						(string)$xmlBlobs[$i]->ContentLanguage,
+						false
+					);
+				}
+			}
+			
+			// Blob prefixes (folders)
+			$xmlBlobs = $this->_parseResponse($response)->Blobs->BlobPrefix;
+			
+			if (!is_null($xmlBlobs)) {
+				for ($i = 0; $i < count($xmlBlobs); $i++) {
+					$blobs[] = new Zend_Service_WindowsAzure_Storage_BlobInstance(
+						$containerName,
+						(string)$xmlBlobs[$i]->Name,
+						'',
+						'',
+						'',
+						0,
+						'',
+						'',
+						'',
+						true
+					);
+				}
+			}
+			
+			// More blobs?
+			$xmlMarker = (string)$this->_parseResponse($response)->NextMarker;
+			$currentResultCount = $currentResultCount + count($blobs);
+			if (!is_null($maxResults) && $currentResultCount < $maxResults) {
+    			if (!is_null($xmlMarker) && $xmlMarker != '') {
+    			    $blobs = array_merge($blobs, $this->listBlobs($containerName, $prefix, $delimiter, $maxResults, $marker, $currentResultCount));
+    			}
+			}
+			if (!is_null($maxResults) && count($blobs) > $maxResults) {
+			    $blobs = array_slice($blobs, 0, $maxResults);
+			}
+			
+			return $blobs;
+		} else {        
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Generate shared access URL
+	 * 
+	 * @param string $containerName  Container name
+	 * @param string $blobName       Blob name
+     * @param string $resource       Signed resource - container (c) - blob (b)
+     * @param string $permissions    Signed permissions - read (r), write (w), delete (d) and list (l)
+     * @param string $start          The time at which the Shared Access Signature becomes valid.
+     * @param string $expiry         The time at which the Shared Access Signature becomes invalid.
+     * @param string $identifier     Signed identifier
+	 * @return string
+	 */
+	public function generateSharedAccessUrl($containerName = '', $blobName = '', $resource = 'b', $permissions = 'r', $start = '', $expiry = '', $identifier = '')
+	{
+		if ($containerName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Container name is not specified.');
+		}
+		if (!self::isValidContainerName($containerName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Container name does not adhere to container naming conventions. See http://msdn.microsoft.com/en-us/library/dd135715.aspx for more information.');
+		}
+
+        // Resource name
+		$resourceName = self::createResourceName($containerName , $blobName);
+
+		// Generate URL
+		return $this->getBaseUrl() . '/' . $resourceName . '?' .
+		    $this->_sharedAccessSignatureCredentials->createSignedQueryString(
+		        $resourceName,
+		        '',
+		        $resource,
+		        $permissions,
+		        $start,
+		        $expiry,
+		        $identifier);
+	}
+	
+    /**
+     * Register this object as stream wrapper client
+     *
+     * @param  string $name Protocol name
+     * @return Zend_Service_WindowsAzure_Storage_Blob
+     */
+    public function registerAsClient($name)
+    {
+        self::$_wrapperClients[$name] = $this;
+        return $this;
+    }
+
+    /**
+     * Unregister this object as stream wrapper client
+     *
+     * @param  string $name Protocol name
+     * @return Zend_Service_WindowsAzure_Storage_Blob
+     */
+    public function unregisterAsClient($name)
+    {
+        unset(self::$_wrapperClients[$name]);
+        return $this;
+    }
+
+    /**
+     * Get wrapper client for stream type
+     *
+     * @param  string $name Protocol name
+     * @return Zend_Service_WindowsAzure_Storage_Blob
+     */
+    public static function getWrapperClient($name)
+    {
+        return self::$_wrapperClients[$name];
+    }
+
+    /**
+     * Register this object as stream wrapper
+     *
+     * @param  string $name Protocol name
+     */
+    public function registerStreamWrapper($name = 'azure')
+    {
+        /**
+         * @see Zend_Service_WindowsAzure_Storage_Blob_Stream
+         */
+        require_once 'Zend/Service/WindowsAzure/Storage/Blob/Stream.php';
+
+        stream_register_wrapper($name, 'Zend_Service_WindowsAzure_Storage_Blob_Stream');
+        $this->registerAsClient($name);
+    }
+
+    /**
+     * Unregister this object as stream wrapper
+     *
+     * @param  string $name Protocol name
+     * @return Zend_Service_WindowsAzure_Storage_Blob
+     */
+    public function unregisterStreamWrapper($name = 'azure')
+    {
+        stream_wrapper_unregister($name);
+        $this->unregisterAsClient($name);
+    }
+    
+    /**
+     * Create resource name
+     * 
+	 * @param string $containerName  Container name
+	 * @param string $blobName Blob name
+     * @return string
+     */
+    public static function createResourceName($containerName = '', $blobName = '')
+    {
+		// Resource name
+		$resourceName = $containerName . '/' . $blobName;
+		if ($containerName === '' || $containerName === '$root') {
+		    $resourceName = $blobName;
+		}
+		if ($blobName === '') {
+		    $resourceName = $containerName;
+		}
+		    
+		return $resourceName;
+    }
+	
+	/**
+	 * Is valid container name?
+	 *
+	 * @param string $containerName Container name
+	 * @return boolean
+	 */
+    public static function isValidContainerName($containerName = '')
+    {
+        if ($containerName == '$root') {
+            return true;
+        }
+            
+        if (!ereg("^[a-z0-9][a-z0-9-]*$", $containerName)) {
+            return false;
+        }
+    
+        if (strpos($containerName, '--') !== false) {
+            return false;
+        }
+    
+        if (strtolower($containerName) != $containerName) {
+            return false;
+        }
+    
+        if (strlen($containerName) < 3 || strlen($containerName) > 63) {
+            return false;
+        }
+            
+        if (substr($containerName, -1) == '-') {
+            return false;
+        }
+    
+        return true;
+    }
+    
+	/**
+	 * Get error message from Zend_Http_Response
+	 * 
+	 * @param Zend_Http_Response $response Repsonse
+	 * @param string $alternativeError Alternative error message
+	 * @return string
+	 */
+	protected function _getErrorMessage(Zend_Http_Response $response, $alternativeError = 'Unknown error.')
+	{
+		$response = $this->_parseResponse($response);
+		if ($response && $response->Message) {
+		    return (string)$response->Message;
+		} else {
+		    return $alternativeError;
+		}
+	}
+	
+	/**
+	 * Generate block id
+	 * 
+	 * @param int $part Block number
+	 * @return string Windows Azure Blob Storage block number
+	 */
+	protected function _generateBlockId($part = 0)
+	{
+		$returnValue = $part;
+		while (strlen($returnValue) < 64) {
+			$returnValue = '0' . $returnValue;
+		}
+		
+		return $returnValue;
+	}
+}

+ 565 - 0
library/Zend/Service/WindowsAzure/Storage/Blob/Stream.php

@@ -0,0 +1,565 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure_Storage
+ * @subpackage Blob
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://todo     name_todo
+ * @version    $Id: Blob.php 24511 2009-07-28 09:17:56Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_Blob
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/Blob.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure_Storage
+ * @subpackage Blob
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_Blob_Stream
+{
+    /**
+     * Current file name
+     * 
+     * @var string
+     */
+    private $_fileName = null;
+    
+    /**
+     * Temporary file name
+     * 
+     * @var string
+     */
+    private $_temporaryFileName = null;
+    
+    /**
+     * Temporary file handle
+     * 
+     * @var resource
+     */
+    private $_temporaryFileHandle = null;
+    
+    /**
+     * Blob storage client
+     * 
+     * @var Zend_Service_WindowsAzure_Storage_Blob
+     */
+    private $_storageClient = null;
+    
+    /**
+     * Write mode?
+     * 
+     * @var boolean
+     */
+    private $_writeMode = false;
+    
+    /**
+     * List of blobs
+     * 
+     * @var array
+     */
+    private $_blobs = null;
+    
+    /**
+     * Retrieve storage client for this stream type
+     * 
+     * @param string $path
+     * @return Zend_Service_WindowsAzure_Storage_Blob
+     */
+    protected function _getStorageClient($path = '')
+    {
+        if (is_null($this->_storageClient)) {
+            $url = explode(':', $path);
+            if (!$url) {
+                throw new Zend_Service_WindowsAzure_Exception('Could not parse path "' . $path . '".');
+            }
+
+            $this->_storageClient = Zend_Service_WindowsAzure_Storage_Blob::getWrapperClient($url[0]);
+            if (!$this->_storageClient) {
+                throw new Zend_Service_WindowsAzure_Exception('No storage client registered for stream type "' . $url[0] . '://".');
+            }
+        }
+        
+        return $this->_storageClient;
+    }
+    
+    /**
+     * Extract container name
+     *
+     * @param string $path
+     * @return string
+     */
+    protected function _getContainerName($path)
+    {
+        $url = parse_url($path);
+        if ($url['host']) {
+            return $url['host'];
+        }
+
+        return '';
+    }
+    
+    /**
+     * Extract file name
+     *
+     * @param string $path
+     * @return string
+     */
+    protected function _getFileName($path)
+    {
+        $url = parse_url($path);
+        if ($url['host']) {
+            $fileName = isset($url['path']) ? $url['path'] : $url['host'];
+    	    if (strpos($fileName, '/') === 0) {
+    	        $fileName = substr($fileName, 1);
+    	    }
+            return $fileName;
+        }
+
+        return '';
+    }
+       
+    /**
+     * Open the stream
+     *
+     * @param  string  $path
+     * @param  string  $mode
+     * @param  integer $options
+     * @param  string  $opened_path
+     * @return boolean
+     */
+    public function stream_open($path, $mode, $options, $opened_path)
+    {
+        $this->_fileName = $path;
+        $this->_temporaryFileName = tempnam(sys_get_temp_dir(), 'azure');
+        
+        // Check the file can be opened
+        $fh = @fopen($this->_temporaryFileName, $mode);
+        if ($fh === false) {
+            return false;
+        }
+        fclose($fh);
+        
+        // Write mode?
+        if (strpbrk($mode, 'wax+')) {
+            $this->_writeMode = true;
+    	} else {
+            $this->_writeMode = false;
+        }
+        
+        // If read/append, fetch the file
+        if (!$this->_writeMode || strpbrk($mode, 'ra+')) {
+            $this->_getStorageClient($this->_fileName)->getBlob(
+                $this->_getContainerName($this->_fileName),
+                $this->_getFileName($this->_fileName),
+                $this->_temporaryFileName
+            );
+        }
+        
+        // Open temporary file handle
+        $this->_temporaryFileHandle = fopen($this->_temporaryFileName, $mode);
+        
+        // Ok!
+        return true;
+    }
+
+    /**
+     * Close the stream
+     *
+     * @return void
+     */
+    public function stream_close()
+    {
+        @fclose($this->_temporaryFileHandle);
+        
+        // Upload the file?
+        if ($this->_writeMode) {
+            // Make sure the container exists
+            $containerExists = $this->_getStorageClient($this->_fileName)->containerExists(
+                $this->_getContainerName($this->_fileName)
+            );
+            if (!$containerExists) {
+                $this->_getStorageClient($this->_fileName)->createContainer(
+                    $this->_getContainerName($this->_fileName)
+                );
+            }
+            
+            // Upload the file
+            try {
+                $this->_getStorageClient($this->_fileName)->putBlob(
+                    $this->_getContainerName($this->_fileName),
+                    $this->_getFileName($this->_fileName),
+                    $this->_temporaryFileName
+                );
+            } catch (Zend_Service_WindowsAzure_Exception $ex) {
+                @unlink($this->_temporaryFileName);
+                unset($this->_storageClient);
+                
+                throw $ex;
+            }
+        }
+        
+        @unlink($this->_temporaryFileName);
+        unset($this->_storageClient);
+    }
+
+    /**
+     * Read from the stream
+     *
+     * @param  integer $count
+     * @return string
+     */
+    public function stream_read($count)
+    {
+        if (!$this->_temporaryFileHandle) {
+            return false;
+        }
+
+        return fread($this->_temporaryFileHandle, $count);
+    }
+
+    /**
+     * Write to the stream
+     *
+     * @param  string $data
+     * @return integer
+     */
+    public function stream_write($data)
+    {
+        if (!$this->_temporaryFileHandle) {
+            return 0;
+        }
+        
+        $len = strlen($data);
+        fwrite($this->_temporaryFileHandle, $data, $len);
+        return $len;
+    }
+
+    /**
+     * End of the stream?
+     *
+     * @return boolean
+     */
+    public function stream_eof()
+    {
+        if (!$this->_temporaryFileHandle) {
+            return true;
+        }
+
+        return feof($this->_temporaryFileHandle);
+    }
+
+    /**
+     * What is the current read/write position of the stream?
+     *
+     * @return integer
+     */
+    public function stream_tell()
+    {
+        return ftell($this->_temporaryFileHandle);
+    }
+
+    /**
+     * Update the read/write position of the stream
+     *
+     * @param  integer $offset
+     * @param  integer $whence
+     * @return boolean
+     */
+    public function stream_seek($offset, $whence)
+    {
+        if (!$this->_temporaryFileHandle) {
+            return false;
+        }
+        
+        return (fseek($this->_temporaryFileHandle, $offset, $whence) === 0);
+    }
+
+    /**
+     * Flush current cached stream data to storage
+     *
+     * @return boolean
+     */
+    public function stream_flush()
+    {
+        $result = fflush($this->_temporaryFileHandle);
+        
+         // Upload the file?
+        if ($this->_writeMode) {
+            // Make sure the container exists
+            $containerExists = $this->_getStorageClient($this->_fileName)->containerExists(
+                $this->_getContainerName($this->_fileName)
+            );
+            if (!$containerExists) {
+                $this->_getStorageClient($this->_fileName)->createContainer(
+                    $this->_getContainerName($this->_fileName)
+                );
+            }
+            
+            // Upload the file
+            try {
+                $this->_getStorageClient($this->_fileName)->putBlob(
+                    $this->_getContainerName($this->_fileName),
+                    $this->_getFileName($this->_fileName),
+                    $this->_temporaryFileName
+                );
+            } catch (Zend_Service_WindowsAzure_Exception $ex) {
+                @unlink($this->_temporaryFileName);
+                unset($this->_storageClient);
+                
+                throw $ex;
+            }
+        }
+        
+        return $result;
+    }
+
+    /**
+     * Returns data array of stream variables
+     *
+     * @return array
+     */
+    public function stream_stat()
+    {
+        if (!$this->_temporaryFileHandle) {
+            return false;
+        }
+
+        $stat = array();
+        $stat['dev'] = 0;
+        $stat['ino'] = 0;
+        $stat['mode'] = 0;
+        $stat['nlink'] = 0;
+        $stat['uid'] = 0;
+        $stat['gid'] = 0;
+        $stat['rdev'] = 0;
+        $stat['size'] = 0;
+        $stat['atime'] = 0;
+        $stat['mtime'] = 0;
+        $stat['ctime'] = 0;
+        $stat['blksize'] = 0;
+        $stat['blocks'] = 0;
+
+        $info = null;
+        try {
+            $info = $this->_getStorageClient($this->_fileName)->getBlobInstance(
+                        $this->_getContainerName($this->_fileName),
+                        $this->_getFileName($this->_fileName)
+                    );
+        } catch (Zend_Service_WindowsAzure_Exception $ex) {
+            // Unexisting file...
+        }
+        if (!is_null($info)) {
+            $stat['size']  = $info->Size;
+            $stat['atime'] = time();
+        }     
+        
+        return $stat;
+    }
+
+    /**
+     * Attempt to delete the item
+     *
+     * @param  string $path
+     * @return boolean
+     */
+    public function unlink($path)
+    {
+        $this->_getStorageClient($path)->deleteBlob(
+            $this->_getContainerName($path),
+            $this->_getFileName($path)
+        );
+    }
+
+    /**
+     * Attempt to rename the item
+     *
+     * @param  string  $path_from
+     * @param  string  $path_to
+     * @return boolean False
+     */
+    public function rename($path_from, $path_to)
+    {
+        if ($this->_getContainerName($path_from) != $this->_getContainerName($path_to)) {
+            throw new Zend_Service_WindowsAzure_Exception('Container name can not be changed.');
+        }
+        
+        if ($this->_getFileName($path_from) == $this->_getContainerName($path_to)) {
+            return true;
+        }
+            
+        $this->_getStorageClient($path_from)->copyBlob(
+            $this->_getContainerName($path_from),
+            $this->_getFileName($path_from),
+            $this->_getContainerName($path_to),
+            $this->_getFileName($path_to)
+        );
+        $this->_getStorageClient($path_from)->deleteBlob(
+            $this->_getContainerName($path_from),
+            $this->_getFileName($path_from)
+        );
+        return true;
+    }
+    
+    /**
+     * Return array of URL variables
+     *
+     * @param  string $path
+     * @param  integer $flags
+     * @return array
+     */
+    public function url_stat($path, $flags)
+    {
+        $stat = array();
+        $stat['dev'] = 0;
+        $stat['ino'] = 0;
+        $stat['mode'] = 0;
+        $stat['nlink'] = 0;
+        $stat['uid'] = 0;
+        $stat['gid'] = 0;
+        $stat['rdev'] = 0;
+        $stat['size'] = 0;
+        $stat['atime'] = 0;
+        $stat['mtime'] = 0;
+        $stat['ctime'] = 0;
+        $stat['blksize'] = 0;
+        $stat['blocks'] = 0;
+
+        $info = null;
+        try {
+            $info = $this->_getStorageClient($path)->getBlobInstance(
+                        $this->_getContainerName($path),
+                        $this->_getFileName($path)
+                    );
+        } catch (Zend_Service_WindowsAzure_Exception $ex) {
+            // Unexisting file...
+        }
+        if (!is_null($info)) {
+            $stat['size']  = $info->Size;
+            $stat['atime'] = time();
+        } 
+
+        return $stat;
+    }
+
+    /**
+     * Create a new directory
+     *
+     * @param  string  $path
+     * @param  integer $mode
+     * @param  integer $options
+     * @return boolean
+     */
+    public function mkdir($path, $mode, $options)
+    {
+        if ($this->_getContainerName($path) == $this->_getFileName($path)) {
+            // Create container
+            try {
+                $this->_getStorageClient($path)->createContainer(
+                    $this->_getContainerName($path)
+                );
+            } catch (Zend_Service_WindowsAzure_Exception $ex) {
+                return false;
+            }
+        } else {
+            throw new Zend_Service_WindowsAzure_Exception('mkdir() with multiple levels is not supported on Windows Azure Blob Storage.');
+        }
+    }
+
+    /**
+     * Remove a directory
+     *
+     * @param  string  $path
+     * @param  integer $options
+     * @return boolean
+     */
+    public function rmdir($path, $options)
+    {
+        if ($this->_getContainerName($path) == $this->_getFileName($path)) {
+            // Delete container
+            try {
+                $this->_getStorageClient($path)->deleteContainer(
+                    $this->_getContainerName($path)
+                );
+            } catch (Zend_Service_WindowsAzure_Exception $ex) {
+                return false;
+            }
+        } else {
+            throw new Zend_Service_WindowsAzure_Exception('rmdir() with multiple levels is not supported on Windows Azure Blob Storage.');
+        }
+    }
+
+    /**
+     * Attempt to open a directory
+     *
+     * @param  string $path
+     * @param  integer $options
+     * @return boolean
+     */
+    public function dir_opendir($path, $options)
+    {
+        $this->_blobs = $this->_getStorageClient($path)->listBlobs(
+            $this->_getContainerName($path)
+        );
+        return is_array($this->_blobs);
+    }
+
+    /**
+     * Return the next filename in the directory
+     *
+     * @return string
+     */
+    public function dir_readdir()
+    {
+        $object = current($this->_blobs);
+        if ($object !== false) {
+            next($this->_blobs);
+            return $object->Name;
+        }
+        return false;
+    }
+
+    /**
+     * Reset the directory pointer
+     *
+     * @return boolean True
+     */
+    public function dir_rewinddir()
+    {
+        reset($this->_blobs);
+        return true;
+    }
+
+    /**
+     * Close a directory
+     *
+     * @return boolean True
+     */
+    public function dir_closedir()
+    {
+        $this->_blobs = null;
+        return true;
+    }
+}

+ 95 - 0
library/Zend/Service/WindowsAzure/Storage/BlobContainer.php

@@ -0,0 +1,95 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobContainer.php 28585 2009-09-07 12:12:56Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * 
+ * @property string $Name          Name of the container
+ * @property string $Etag          Etag of the container
+ * @property string $LastModified  Last modified date of the container
+ * @property array  $Metadata      Key/value pairs of meta data
+ */
+class Zend_Service_WindowsAzure_Storage_BlobContainer
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string $name          Name
+     * @param string $etag          Etag
+     * @param string $lastModified  Last modified date
+     * @param array  $metadata      Key/value pairs of meta data
+     */
+    public function __construct($name, $etag, $lastModified, $metadata = array()) 
+    {
+        $this->_data = array(
+            'name'         => $name,
+            'etag'         => $etag,
+            'lastmodified' => $lastModified,
+            'metadata'     => $metadata
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 116 - 0
library/Zend/Service/WindowsAzure/Storage/BlobInstance.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobInstance.php 28585 2009-09-07 12:12:56Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * 
+ * @property string  $Container       Container name
+ * @property string  $Name            Name
+ * @property string  $Etag            Etag
+ * @property string  $LastModified    Last modified date
+ * @property string  $Url             Url
+ * @property int     $Size            Size
+ * @property string  $ContentType     Content Type
+ * @property string  $ContentEncoding Content Encoding
+ * @property string  $ContentLanguage Content Language
+ * @property boolean $IsPrefix        Is Prefix?
+ * @property array   $Metadata        Key/value pairs of meta data
+ */
+class Zend_Service_WindowsAzure_Storage_BlobInstance
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string  $containerName   Container name
+     * @param string  $name            Name
+     * @param string  $etag            Etag
+     * @param string  $lastModified    Last modified date
+     * @param string  $url             Url
+     * @param int     $size            Size
+     * @param string  $contentType     Content Type
+     * @param string  $contentEncoding Content Encoding
+     * @param string  $contentLanguage Content Language
+     * @param boolean $isPrefix        Is Prefix?
+     * @param array   $metadata        Key/value pairs of meta data
+     */
+    public function __construct($containerName, $name, $etag, $lastModified, $url = '', $size = 0, $contentType = '', $contentEncoding = '', $contentLanguage = '', $isPrefix = false, $metadata = array()) 
+    {	        
+        $this->_data = array(
+            'container'        => $containerName,
+            'name'             => $name,
+            'etag'             => $etag,
+            'lastmodified'     => $lastModified,
+            'url'              => $url,
+            'size'             => $size,
+            'contenttype'      => $contentType,
+            'contentencoding'  => $contentEncoding,
+            'contentlanguage'  => $contentLanguage,
+            'isprefix'         => $isPrefix,
+            'metadata'         => $metadata
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 200 - 0
library/Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php

@@ -0,0 +1,200 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobInstance.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_TableEntity
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/TableEntity.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_DynamicTableEntity extends Zend_Service_WindowsAzure_Storage_TableEntity
+{   
+    /**
+     * Dynamic properties
+     * 
+     * @var array
+     */
+    protected $_dynamicProperties = array();
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {      
+        $this->setAzureProperty($name, $value, null);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        return $this->getAzureProperty($name);
+    }
+    
+    /**
+     * Set an Azure property
+     * 
+     * @param string $name Property name
+     * @param mixed $value Property value
+     * @param string $type Property type (Edm.xxxx)
+     * @return Zend_Service_WindowsAzure_Storage_DynamicTableEntity
+     */
+    public function setAzureProperty($name, $value = '', $type = null)
+    {
+        if (strtolower($name) == 'partitionkey') {
+            $this->setPartitionKey($value);
+        } else if (strtolower($name) == 'rowkey') {
+            $this->setRowKey($value);
+        } else if (strtolower($name) == 'etag') {
+            $this->setEtag($value);
+        } else {
+            if (!array_key_exists(strtolower($name), $this->_dynamicProperties)) {
+                // Determine type?
+                if (is_null($type)) {
+                    $type = 'Edm.String';
+                    if (is_int($value)) {
+                        $type = 'Edm.Int32';
+                    } else if (is_float($value)) {
+                        $type = 'Edm.Double';
+                    } else if (is_bool($value)) {
+                        $type = 'Edm.Boolean';
+                    }
+                }
+                
+                // Set dynamic property
+                $this->_dynamicProperties[strtolower($name)] = (object)array(
+                        'Name'  => $name,
+                    	'Type'  => $type,
+                    	'Value' => $value,
+                    );
+            }
+    
+            $this->_dynamicProperties[strtolower($name)]->Value = $value;
+        }
+        return $this;
+    }
+    
+    /**
+     * Set an Azure property type
+     * 
+     * @param string $name Property name
+     * @param string $type Property type (Edm.xxxx)
+     * @return Zend_Service_WindowsAzure_Storage_DynamicTableEntity
+     */
+    public function setAzurePropertyType($name, $type = 'Edm.String')
+    {
+        if (!array_key_exists(strtolower($name), $this->_dynamicProperties)) {
+            $this->setAzureProperty($name, '', $type);            
+        } else {
+            $this->_dynamicProperties[strtolower($name)]->Type = $type;   
+        }
+        return $this;
+    }
+    
+    /**
+     * Get an Azure property
+     * 
+     * @param string $name Property name
+     * @param mixed $value Property value
+     * @param string $type Property type (Edm.xxxx)
+     * @return Zend_Service_WindowsAzure_Storage_DynamicTableEntity
+     */
+    public function getAzureProperty($name)
+    {
+        if (strtolower($name) == 'partitionkey') {
+            return $this->getPartitionKey();
+        }
+        if (strtolower($name) == 'rowkey') {
+            return $this->getRowKey();
+        }
+        if (strtolower($name) == 'etag') {
+            return $this->getEtag();
+        }
+
+        if (!array_key_exists(strtolower($name), $this->_dynamicProperties)) {
+            $this->setAzureProperty($name);            
+        }
+
+        return $this->_dynamicProperties[strtolower($name)]->Value;
+    }
+    
+    /**
+     * Get an Azure property type
+     * 
+     * @param string $name Property name
+     * @return string Property type (Edm.xxxx)
+     */
+    public function getAzurePropertyType($name)
+    {
+        if (!array_key_exists(strtolower($name), $this->_dynamicProperties)) {
+            $this->setAzureProperty($name, '', $type);            
+        }
+        
+        return $this->_dynamicProperties[strtolower($name)]->Type;
+    }
+    
+    /**
+     * Get Azure values
+     * 
+     * @return array
+     */
+    public function getAzureValues()
+    {
+        return array_merge(array_values($this->_dynamicProperties), parent::getAzureValues());
+    }
+    
+    /**
+     * Set Azure values
+     * 
+     * @param array $values
+     * @param boolean $throwOnError Throw Zend_Service_WindowsAzure_Exception when a property is not specified in $values?
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    public function setAzureValues($values = array(), $throwOnError = false)
+    {
+        // Set parent values
+        parent::setAzureValues($values, false);
+        
+        // Set current values
+        foreach ($values as $key => $value) 
+        {
+            $this->$key = $value;
+        }
+    }
+}

+ 556 - 0
library/Zend/Service/WindowsAzure/Storage/Queue.php

@@ -0,0 +1,556 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://todo     name_todo
+ * @version    $Id: Blob.php 24241 2009-07-22 09:43:13Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_SharedKeyCredentials
+ */
+require_once 'Zend/Service/WindowsAzure/SharedKeyCredentials.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @see Zend_Http_Response
+ */
+require_once 'Zend/Http/Response.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * Zend_Service_WindowsAzure_Storage_QueueInstance
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/QueueInstance.php';
+
+/**
+ * Zend_Service_WindowsAzure_Storage_QueueMessage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/QueueMessage.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_Queue extends Zend_Service_WindowsAzure_Storage
+{
+	/**
+	 * Maximal message size (in bytes)
+	 */
+	const MAX_MESSAGE_SIZE = 8388608;
+	
+	/**
+	 * Maximal message ttl (in seconds)
+	 */
+	const MAX_MESSAGE_TTL = 604800;
+	
+	/**
+	 * Creates a new Zend_Service_WindowsAzure_Storage_Queue instance
+	 *
+	 * @param string $host Storage host name
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
+	 */
+	public function __construct($host = Zend_Service_WindowsAzure_Storage::URL_DEV_QUEUE, $accountName = Zend_Service_WindowsAzure_SharedKeyCredentials::DEVSTORE_ACCOUNT, $accountKey = Zend_Service_WindowsAzure_SharedKeyCredentials::DEVSTORE_KEY, $usePathStyleUri = false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null)
+	{
+		parent::__construct($host, $accountName, $accountKey, $usePathStyleUri, $retryPolicy);
+		
+		// API version
+		$this->_apiVersion = '2009-04-14';
+	}
+	
+	/**
+	 * Check if a queue exists
+	 * 
+	 * @param string $queueName Queue name
+	 * @return boolean
+	 */
+	public function queueExists($queueName = '')
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+			
+		// List queues
+        $queues = $this->listQueues($queueName, 1);
+        foreach ($queues as $queue) {
+            if ($queue->Name == $queueName) {
+                return true;
+            }
+        }
+        
+        return false;
+	}
+	
+	/**
+	 * Create queue
+	 *
+	 * @param string $queueName Queue name
+	 * @param array  $metadata  Key/value pairs of meta data
+	 * @return object Queue properties
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function createQueue($queueName = '', $metadata = array())
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+			
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($queueName, '', Zend_Http_Client::PUT, $headers);			
+		if ($response->isSuccessful()) {
+		    return new Zend_Service_WindowsAzure_Storage_QueueInstance(
+		        $queueName,
+		        $metadata
+		    );
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get queue
+	 * 
+	 * @param string $queueName  Queue name
+	 * @return Zend_Service_WindowsAzure_Storage_QueueInstance
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getQueue($queueName = '')
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+		    
+		// Perform request
+		$response = $this->_performRequest($queueName, '?comp=metadata', Zend_Http_Client::GET);	
+		if ($response->isSuccessful()) {
+		    // Parse metadata
+		    $metadata = array();
+		    foreach ($response->getHeaders() as $key => $value) {
+		        if (substr(strtolower($key), 0, 10) == "x-ms-meta-") {
+		            $metadata[str_replace("x-ms-meta-", '', strtolower($key))] = $value;
+		        }
+		    }
+
+		    // Return queue
+		    $queue = new Zend_Service_WindowsAzure_Storage_QueueInstance(
+		        $queueName,
+		        $metadata
+		    );
+		    $queue->ApproximateMessageCount = intval($response->getHeader('x-ms-approximate-message-count'));
+		    return $queue;
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Get queue metadata
+	 * 
+	 * @param string $queueName  Queue name
+	 * @return array Key/value pairs of meta data
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getQueueMetadata($queueName = '')
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+			
+	    return $this->getQueue($queueName)->Metadata;
+	}
+	
+	/**
+	 * Set queue metadata
+	 * 
+	 * Calling the Set Queue Metadata operation overwrites all existing metadata that is associated with the queue. It's not possible to modify an individual name/value pair.
+	 *
+	 * @param string $queueName  Queue name
+	 * @param array  $metadata       Key/value pairs of meta data
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function setQueueMetadata($queueName = '', $metadata = array())
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+		if (count($metadata) == 0) {
+		    return;
+		}
+		    
+		// Create metadata headers
+		$headers = array();
+		foreach ($metadata as $key => $value) {
+		    $headers["x-ms-meta-" . strtolower($key)] = $value;
+		}
+		
+		// Perform request
+		$response = $this->_performRequest($queueName, '?comp=metadata', Zend_Http_Client::PUT, $headers);
+
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Delete queue
+	 *
+	 * @param string $queueName Queue name
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteQueue($queueName = '')
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+			
+		// Perform request
+		$response = $this->_performRequest($queueName, '', Zend_Http_Client::DELETE);
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * List queues
+	 *
+	 * @param string $prefix     Optional. Filters the results to return only queues whose name begins with the specified prefix.
+	 * @param int    $maxResults Optional. Specifies the maximum number of queues to return per call to Azure storage. This does NOT affect list size returned by this function. (maximum: 5000)
+	 * @param string $marker     Optional string value that identifies the portion of the list to be returned with the next list operation.
+	 * @param int    $currentResultCount Current result count (internal use)
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function listQueues($prefix = null, $maxResults = null, $marker = null, $currentResultCount = 0)
+	{
+	    // Build query string
+	    $queryString = '?comp=list';
+	    if (!is_null($prefix)) {
+	        $queryString .= '&prefix=' . $prefix;
+	    }
+	    if (!is_null($maxResults)) {
+	        $queryString .= '&maxresults=' . $maxResults;
+	    }
+	    if (!is_null($marker)) {
+	        $queryString .= '&marker=' . $marker;
+	    }
+	        
+		// Perform request
+		$response = $this->_performRequest('', $queryString, Zend_Http_Client::GET);	
+		if ($response->isSuccessful()) {
+			$xmlQueues = $this->_parseResponse($response)->Queues->Queue;
+			$xmlMarker = (string) $this->_parseResponse($response)->NextMarker;
+
+			$queues = array();
+			if (!is_null($xmlQueues)) {
+				for ($i = 0; $i < count($xmlQueues); $i++) {
+					$queues[] = new Zend_Service_WindowsAzure_Storage_QueueInstance(
+						(string)$xmlQueues[$i]->QueueName
+					);
+				}
+			}
+			$currentResultCount = $currentResultCount + count($queues);
+			if (!is_null($maxResults) && $currentResultCount < $maxResults) {
+    			if (!is_null($xmlMarker) && $xmlMarker != '') {
+    			    $queues = array_merge($queues, $this->listQueues($prefix, $maxResults, $xmlMarker, $currentResultCount));
+    			}
+			}
+			if (!is_null($maxResults) && count($queues) > $maxResults) {
+			    $queues = array_slice($queues, 0, $maxResults);
+			}
+			    
+			return $queues;
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Put message into queue
+	 *
+	 * @param string $queueName  Queue name
+	 * @param string $message    Message
+	 * @param int    $ttl        Message Time-To-Live (in seconds). Defaults to 7 days if the parameter is omitted.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function putMessage($queueName = '', $message = '', $ttl = null)
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+		if (strlen($message) > self::MAX_MESSAGE_SIZE) {
+		    throw new Zend_Service_WindowsAzure_Exception('Message is too big. Message content should be < 8KB.');
+		}
+		if ($message == '') {
+		    throw new Zend_Service_WindowsAzure_Exception('Message is not specified.');
+		}
+		if (!is_null($ttl) && ($ttl <= 0 || $ttl > self::MAX_MESSAGE_SIZE)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Message TTL is invalid. Maximal TTL is 7 days (' . self::MAX_MESSAGE_SIZE . ' seconds) and should be greater than zero.');
+		}
+		    
+	    // Build query string
+	    $queryString = '';
+	    if (!is_null($ttl)) {
+	        $queryString .= '?messagettl=' . $ttl;
+	    }
+	        
+	    // Build body
+	    $rawData = '';
+	    $rawData .= '<QueueMessage>';
+	    $rawData .= '    <MessageText>' . base64_encode($message) . '</MessageText>';
+	    $rawData .= '</QueueMessage>';
+	        
+		// Perform request
+		$response = $this->_performRequest($queueName . '/messages', $queryString, Zend_Http_Client::POST, array(), false, $rawData);
+
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception('Error putting message into queue.');
+		}
+	}
+	
+	/**
+	 * Get queue messages
+	 *
+	 * @param string $queueName         Queue name
+	 * @param string $numOfMessages     Optional. A nonzero integer value that specifies the number of messages to retrieve from the queue, up to a maximum of 32. By default, a single message is retrieved from the queue with this operation.
+	 * @param int    $visibilityTimeout Optional. An integer value that specifies the message's visibility timeout in seconds. The maximum value is 2 hours. The default message visibility timeout is 30 seconds.
+	 * @param string $peek              Peek only?
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function getMessages($queueName = '', $numOfMessages = 1, $visibilityTimeout = null, $peek = false)
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+		if ($numOfMessages < 1 || $numOfMessages > 32 || intval($numOfMessages) != $numOfMessages) {
+		    throw new Zend_Service_WindowsAzure_Exception('Invalid number of messages to retrieve.');
+		}
+		if (!is_null($visibilityTimeout) && ($visibilityTimeout <= 0 || $visibilityTimeout > 7200)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Visibility timeout is invalid. Maximum value is 2 hours (7200 seconds) and should be greater than zero.');
+		}
+		    
+	    // Build query string
+	    $query = array();
+    	if ($peek) {
+    	    $query[] = 'peekonly=true';
+    	}
+    	if ($numOfMessages > 1) {
+	        $query[] = 'numofmessages=' . $numOfMessages;
+    	}
+    	if (!$peek && !is_null($visibilityTimeout)) {
+	        $query[] = 'visibilitytimeout=' . $visibilityTimeout;
+    	}   
+    	$queryString = '?' . implode('&', $query);
+	        
+		// Perform request
+		$response = $this->_performRequest($queueName . '/messages', $queryString, Zend_Http_Client::GET);	
+		if ($response->isSuccessful()) {
+		    // Parse results
+			$result = $this->_parseResponse($response);
+		    if (!$result) {
+		        return array();
+		    }
+
+		    $xmlMessages = null;
+		    if (count($result->QueueMessage) > 1) {
+    		    $xmlMessages = $result->QueueMessage;
+    		} else {
+    		    $xmlMessages = array($result->QueueMessage);
+    		}
+
+			$messages = array();
+			for ($i = 0; $i < count($xmlMessages); $i++) {
+				$messages[] = new Zend_Service_WindowsAzure_Storage_QueueMessage(
+					(string)$xmlMessages[$i]->MessageId,
+					(string)$xmlMessages[$i]->InsertionTime,
+					(string)$xmlMessages[$i]->ExpirationTime,
+					($peek ? '' : (string)$xmlMessages[$i]->PopReceipt),
+					($peek ? '' : (string)$xmlMessages[$i]->TimeNextVisible),
+					base64_decode((string)$xmlMessages[$i]->MessageText)
+			    );
+			}
+			    
+			return $messages;
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Peek queue messages
+	 *
+	 * @param string $queueName         Queue name
+	 * @param string $numOfMessages     Optional. A nonzero integer value that specifies the number of messages to retrieve from the queue, up to a maximum of 32. By default, a single message is retrieved from the queue with this operation.
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function peekMessages($queueName = '', $numOfMessages = 1)
+	{
+	    return $this->getMessages($queueName, $numOfMessages, null, true);
+	}
+	
+	/**
+	 * Clear queue messages
+	 *
+	 * @param string $queueName         Queue name
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function clearMessages($queueName = '')
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+
+		// Perform request
+		$response = $this->_performRequest($queueName . '/messages', '', Zend_Http_Client::DELETE);	
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception('Error clearing messages from queue.');
+		}
+	}
+	
+	/**
+	 * Delete queue message
+	 *
+	 * @param string $queueName         					Queue name
+	 * @param Zend_Service_WindowsAzure_Storage_QueueMessage $message Message to delete from queue. A message retrieved using "peekMessages" can NOT be deleted!
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteMessage($queueName = '', Zend_Service_WindowsAzure_Storage_QueueMessage $message)
+	{
+		if ($queueName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Queue name is not specified.');
+		}
+		if (!self::isValidQueueName($queueName)) {
+		    throw new Zend_Service_WindowsAzure_Exception('Queue name does not adhere to queue naming conventions. See http://msdn.microsoft.com/en-us/library/dd179349.aspx for more information.');
+		}
+		if ($message->PopReceipt == '') {
+		    throw new Zend_Service_WindowsAzure_Exception('A message retrieved using "peekMessages" can NOT be deleted! Use "getMessages" instead.');
+		}
+
+		// Perform request
+		$response = $this->_performRequest($queueName . '/messages/' . $message->MessageId, '?popreceipt=' . $message->PopReceipt, Zend_Http_Client::DELETE);	
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Is valid queue name?
+	 *
+	 * @param string $queueName Queue name
+	 * @return boolean
+	 */
+    public static function isValidQueueName($queueName = '')
+    {
+        if (!ereg("^[a-z0-9][a-z0-9-]*$", $queueName)) {
+            return false;
+        }
+    
+        if (strpos($queueName, '--') !== false) {
+            return false;
+        }
+    
+        if (strtolower($queueName) != $queueName) {
+            return false;
+        }
+    
+        if (strlen($queueName) < 3 || strlen($queueName) > 63) {
+            return false;
+        }
+            
+        if (substr($queueName, -1) == '-') {
+            return false;
+        }
+    
+        return true;
+    }
+    
+	/**
+	 * Get error message from Zend_Http_Response
+	 * 
+	 * @param Zend_Http_Response $response Repsonse
+	 * @param string $alternativeError Alternative error message
+	 * @return string
+	 */
+	protected function _getErrorMessage(Zend_Http_Response $response, $alternativeError = 'Unknown error.')
+	{
+		$response = $this->_parseResponse($response);
+		if ($response && $response->Message) {
+		    return (string)$response->Message;
+		} else {
+		    return $alternativeError;
+		}
+	}
+}

+ 91 - 0
library/Zend/Service/WindowsAzure/Storage/QueueInstance.php

@@ -0,0 +1,91 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobContainer.php 17553 2009-05-15 10:40:55Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * 
+ * @property string  $Name                     Name of the queue
+ * @property array   $Metadata                 Key/value pairs of meta data
+ * @property integer $ApproximateMessageCount  The approximate number of messages in the queue
+ */
+class Zend_Service_WindowsAzure_Storage_QueueInstance
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string $name          Name
+     * @param array  $metadata      Key/value pairs of meta data
+     */
+    public function __construct($name, $metadata = array()) 
+    {
+        $this->_data = array(
+            'name'         => $name,
+            'metadata'     => $metadata,
+            'approximatemessagecount' => 0
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 101 - 0
library/Zend/Service/WindowsAzure/Storage/QueueMessage.php

@@ -0,0 +1,101 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobContainer.php 17553 2009-05-15 10:40:55Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ *   
+ * @property string $MessageId         Message ID
+ * @property string $InsertionTime     Insertion time
+ * @property string $ExpirationTime    Expiration time
+ * @property string $PopReceipt  	   Receipt verification for deleting the message from queue.
+ * @property string $TimeNextVisible   Next time the message is visible in the queue
+ * @property string $MessageText       Message text
+ */
+class Zend_Service_WindowsAzure_Storage_QueueMessage
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string $messageId         Message ID
+     * @param string $insertionTime     Insertion time
+     * @param string $expirationTime    Expiration time
+     * @param string $popReceipt  	    Receipt verification for deleting the message from queue.
+     * @param string $timeNextVisible   Next time the message is visible in the queue
+     * @param string $messageText       Message text
+     */
+    public function __construct($messageId, $insertionTime, $expirationTime, $popReceipt, $timeNextVisible, $messageText) 
+    {
+        $this->_data = array(
+            'messageid'       => $messageId,
+            'insertiontime'   => $insertionTime,
+            'expirationtime'  => $expirationTime,
+            'popreceipt'      => $popReceipt,
+            'timenextvisible' => $timeNextVisible,
+            'messagetext'     => $messageText
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 94 - 0
library/Zend/Service/WindowsAzure/Storage/SignedIdentifier.php

@@ -0,0 +1,94 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobContainer.php 24352 2009-07-24 06:44:32Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * 
+ * @property string $Id           Id for the signed identifier
+ * @property string $Start        The time at which the Shared Access Signature becomes valid.
+ * @property string $Expiry       The time at which the Shared Access Signature becomes invalid.
+ * @property string $Permissions  Signed permissions - read (r), write (w), delete (d) and list (l)
+ */
+class Zend_Service_WindowsAzure_Storage_SignedIdentifier
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string $id           Id for the signed identifier
+     * @param string $start        The time at which the Shared Access Signature becomes valid.
+     * @param string $expiry       The time at which the Shared Access Signature becomes invalid.
+     * @param string $permissions  Signed permissions - read (r), write (w), delete (d) and list (l)
+     */
+    public function __construct($id = '', $start = '', $expiry = '', $permissions = '') 
+    {
+        $this->_data = array(
+            'id'           => $id,
+            'start'        => $start,
+            'expiry'       => $expiry,
+            'permissions'  => $permissions
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 806 - 0
library/Zend/Service/WindowsAzure/Storage/Table.php

@@ -0,0 +1,806 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Blob.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_CredentialsAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/CredentialsAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_SharedKey
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKey.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Credentials_SharedKeyLite
+ */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @see Zend_Http_Client
+ */
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @see Zend_Http_Response
+ */
+require_once 'Zend/Http/Response.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage
+ */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_TableInstance
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/TableInstance.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_TableEntity
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/TableEntity.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_DynamicTableEntity
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Storage_TableEntityQuery
+ */
+require_once 'Zend/Service/WindowsAzure/Storage/TableEntityQuery.php';
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_Table 
+    extends Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
+{
+	/**
+	 * Creates a new Zend_Service_WindowsAzure_Storage_Table instance
+	 *
+	 * @param string $host Storage host name
+	 * @param string $accountName Account name for Windows Azure
+	 * @param string $accountKey Account key for Windows Azure
+	 * @param boolean $usePathStyleUri Use path-style URI's
+	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
+	 */
+	public function __construct($host = Zend_Service_WindowsAzure_Storage::URL_DEV_TABLE, $accountName = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT, $accountKey = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY, $usePathStyleUri = false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null)
+	{
+		parent::__construct($host, $accountName, $accountKey, $usePathStyleUri, $retryPolicy);
+
+	    // Always use SharedKeyLite authentication
+	    $this->_credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite($accountName, $accountKey, $this->_usePathStyleUri);
+	    
+	    // API version
+		$this->_apiVersion = '2009-04-14';
+	}
+	
+	/**
+	 * Check if a table exists
+	 * 
+	 * @param string $tableName Table name
+	 * @return boolean
+	 */
+	public function tableExists($tableName = '')
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+			
+		// List tables
+        $tables = $this->listTables($tableName);
+        foreach ($tables as $table) {
+            if ($table->Name == $tableName) {
+                return true;
+            }
+        }
+        
+        return false;
+	}
+	
+	/**
+	 * List tables
+	 *
+	 * @param  string $nextTableName Next table name, used for listing tables when total amount of tables is > 1000.
+	 * @return array
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function listTables($nextTableName = '')
+	{
+	    // Build query string
+	    $queryString = '';
+	    if ($nextTableName != '') {
+	        $queryString = '?NextTableName=' . $nextTableName;
+	    }
+	    
+		// Perform request
+		$response = $this->_performRequest('Tables', $queryString, Zend_Http_Client::GET, null, true);
+		if ($response->isSuccessful()) {	    
+		    // Parse result
+		    $result = $this->_parseResponse($response);	
+		    
+		    if (!$result || !$result->entry) {
+		        return array();
+		    }
+	        
+		    $entries = null;
+		    if (count($result->entry) > 1) {
+		        $entries = $result->entry;
+		    } else {
+		        $entries = array($result->entry);
+		    }
+
+		    // Create return value
+		    $returnValue = array();		    
+		    foreach ($entries as $entry) {
+		        $tableName = $entry->xpath('.//m:properties/d:TableName');
+		        $tableName = (string)$tableName[0];
+		        
+		        $returnValue[] = new Zend_Service_WindowsAzure_Storage_TableInstance(
+		            (string)$entry->id,
+		            $tableName,
+		            (string)$entry->link['href'],
+		            (string)$entry->updated
+		        );
+		    }
+		    
+			// More tables?
+		    if (!is_null($response->getHeader('x-ms-continuation-NextTableName'))) {
+		        $returnValue = array_merge($returnValue, $this->listTables($response->getHeader('x-ms-continuation-NextTableName')));
+		    }
+
+		    return $returnValue;
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Create table
+	 *
+	 * @param string $tableName Table name
+	 * @return Zend_Service_WindowsAzure_Storage_TableInstance
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function createTable($tableName = '')
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+			
+		// Generate request body
+		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+                        <entry
+                        	xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
+                        	xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
+                        	xmlns="http://www.w3.org/2005/Atom">
+                          <title />
+                          <updated>{tpl:Updated}</updated>
+                          <author>
+                            <name />
+                          </author>
+                          <id />
+                          <content type="application/xml">
+                            <m:properties>
+                              <d:TableName>{tpl:TableName}</d:TableName>
+                            </m:properties>
+                          </content>
+                        </entry>';
+		
+        $requestBody = $this->_fillTemplate($requestBody, array(
+            'BaseUrl' => $this->getBaseUrl(),
+            'TableName' => $tableName,
+        	'Updated' => $this->isoDate(),
+            'AccountName' => $this->_accountName
+        ));
+        
+        // Add header information
+        $headers = array();
+        $headers['Content-Type'] = 'application/atom+xml';
+        $headers['DataServiceVersion'] = '1.0;NetFx';
+        $headers['MaxDataServiceVersion'] = '1.0;NetFx';        
+
+		// Perform request
+		$response = $this->_performRequest('Tables', '', Zend_Http_Client::POST, $headers, true, $requestBody);
+		if ($response->isSuccessful()) {
+		    // Parse response
+		    $entry = $this->_parseResponse($response);
+		    
+		    $tableName = $entry->xpath('.//m:properties/d:TableName');
+		    $tableName = (string)$tableName[0];
+		        
+		    return new Zend_Service_WindowsAzure_Storage_TableInstance(
+		        (string)$entry->id,
+		        $tableName,
+		        (string)$entry->link['href'],
+		        (string)$entry->updated
+		    );
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Delete table
+	 *
+	 * @param string $tableName Table name
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteTable($tableName = '')
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+
+        // Add header information
+        $headers = array();
+        $headers['Content-Type'] = 'application/atom+xml';
+
+		// Perform request
+		$response = $this->_performRequest('Tables(\'' . $tableName . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
+		if (!$response->isSuccessful()) {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Insert entity into table
+	 * 
+	 * @param string                              $tableName   Table name
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to insert
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntity
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function insertEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+		if (is_null($entity)) {
+			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
+		}
+		                     
+		// Generate request body
+		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+                        <entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
+                          <title />
+                          <updated>{tpl:Updated}</updated>
+                          <author>
+                            <name />
+                          </author>
+                          <id />
+                          <content type="application/xml">
+                            <m:properties>
+                              {tpl:Properties}
+                            </m:properties>
+                          </content>
+                        </entry>';
+		
+        $requestBody = $this->_fillTemplate($requestBody, array(
+        	'Updated'    => $this->isoDate(),
+            'Properties' => $this->_generateAzureRepresentation($entity)
+        ));
+
+        // Add header information
+        $headers = array();
+        $headers['Content-Type'] = 'application/atom+xml';
+
+		// Perform request
+	    $response = null;
+	    if ($this->isInBatch()) {
+		    $this->getCurrentBatch()->enlistOperation($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
+		    return null;
+		} else {
+		    $response = $this->_performRequest($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
+		}
+		if ($response->isSuccessful()) {
+		    // Parse result
+		    $result = $this->_parseResponse($response);
+		    
+		    $timestamp = $result->xpath('//m:properties/d:Timestamp');
+		    $timestamp = (string)$timestamp[0];
+
+		    $etag      = $result->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
+		    $etag      = (string)$etag['etag'];
+		    
+		    // Update properties
+		    $entity->setTimestamp($timestamp);
+		    $entity->setEtag($etag);
+
+		    return $entity;
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Delete entity from table
+	 * 
+	 * @param string                              $tableName   Table name
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to delete
+	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function deleteEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+		if (is_null($entity)) {
+			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
+		}
+		                     
+        // Add header information
+        $headers = array();
+        if (!$this->isInBatch()) {
+        	// http://social.msdn.microsoft.com/Forums/en-US/windowsazure/thread/9e255447-4dc7-458a-99d3-bdc04bdc5474/
+            $headers['Content-Type']   = 'application/atom+xml';
+        }
+        $headers['Content-Length'] = 0;
+        if (!$verifyEtag) {
+            $headers['If-Match']       = '*';
+        } else {
+            $headers['If-Match']       = $entity->getEtag();
+        }
+
+		// Perform request
+	    $response = null;
+	    if ($this->isInBatch()) {
+		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
+		    return null;
+		} else {
+		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
+		}
+		if (!$response->isSuccessful()) {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Retrieve entity from table, by id
+	 * 
+	 * @param string $tableName    Table name
+	 * @param string $partitionKey Partition key
+	 * @param string $rowKey       Row key
+	 * @param string $entityClass  Entity class name* 
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntity
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function retrieveEntityById($tableName = '', $partitionKey = '', $rowKey = '', $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity')
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+		if ($partitionKey === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Partition key is not specified.');
+		}
+		if ($rowKey === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Row key is not specified.');
+		}
+		if ($entityClass === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
+		}
+
+			
+		// Check for combined size of partition key and row key
+		// http://msdn.microsoft.com/en-us/library/dd179421.aspx
+		if (strlen($partitionKey . $rowKey) >= 256) {
+		    // Start a batch if possible
+		    if ($this->isInBatch()) {
+		        throw new Zend_Service_WindowsAzure_Exception('Entity cannot be retrieved. A transaction is required to retrieve the entity, but another transaction is already active.');
+		    }
+		        
+		    $this->startBatch();
+		}
+		
+		// Fetch entities from Azure
+        $result = $this->retrieveEntities(
+            $this->select()
+                 ->from($tableName)
+                 ->wherePartitionKey($partitionKey)
+                 ->whereRowKey($rowKey),
+            '',
+            $entityClass
+        );
+        
+        // Return
+        if (count($result) == 1) {
+            return $result[0];
+        }
+        
+        return null;
+	}
+	
+	/**
+	 * Create a new Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 * 
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function select()
+	{
+	    return new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+	}
+	
+	/**
+	 * Retrieve entities from table
+	 * 
+	 * @param string $tableName|Zend_Service_WindowsAzure_Storage_TableEntityQuery    Table name -or- Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
+	 * @param string $filter                                                Filter condition (not applied when $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance)
+	 * @param string $entityClass                                           Entity class name
+	 * @param string $nextPartitionKey                                      Next partition key, used for listing entities when total amount of entities is > 1000.
+	 * @param string $nextRowKey                                            Next row key, used for listing entities when total amount of entities is > 1000.
+	 * @return array Array of Zend_Service_WindowsAzure_Storage_TableEntity
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function retrieveEntities($tableName = '', $filter = '', $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity', $nextPartitionKey = null, $nextRowKey = null)
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+		if ($entityClass === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
+		}
+
+		// Convenience...
+		if (class_exists($filter)) {
+		    $entityClass = $filter;
+		    $filter = '';
+		}
+			
+		// Query string
+		$queryString = '';
+
+		// Determine query
+		if (is_string($tableName)) {
+		    // Option 1: $tableName is a string
+		    
+		    // Append parentheses
+		    $tableName .= '()';
+		    
+    	    // Build query
+    	    $query = array();
+    	    
+    		// Filter?
+    		if ($filter !== '') {
+    		    $query[] = '$filter=' . rawurlencode($filter);
+    		}
+    		    
+    	    // Build queryString
+    	    if (count($query) > 0)  {
+    	        $queryString = '?' . implode('&', $query);
+    	    }
+		} else if (get_class($tableName) == 'Zend_Service_WindowsAzure_Storage_TableEntityQuery') {
+		    // Option 2: $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
+
+		    // Build queryString
+		    $queryString = $tableName->assembleQueryString(true);
+
+		    // Change $tableName
+		    $tableName = $tableName->assembleFrom(true);
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception('Invalid argument: $tableName');
+		}
+		
+		// Add continuation querystring parameters?
+		if (!is_null($nextPartitionKey) && !is_null($nextRowKey)) {
+		    if ($queryString !== '') {
+		        $queryString .= '&';
+		    }
+		        
+		    $queryString .= '&NextPartitionKey=' . rawurlencode($nextPartitionKey) . '&NextRowKey=' . rawurlencode($nextRowKey);
+		}
+
+		// Perform request
+	    $response = null;
+	    if ($this->isInBatch() && $this->getCurrentBatch()->getOperationCount() == 0) {
+		    $this->getCurrentBatch()->enlistOperation($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
+		    $response = $this->getCurrentBatch()->commit();
+		    
+		    // Get inner response (multipart)
+		    $innerResponse = $response->getBody();
+		    $innerResponse = substr($innerResponse, strpos($innerResponse, 'HTTP/1.1 200 OK'));
+		    $innerResponse = substr($innerResponse, 0, strpos($innerResponse, '--batchresponse'));
+		    $response = Zend_Http_Response::fromString($innerResponse);
+		} else {
+		    $response = $this->_performRequest($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
+		}
+		
+		if ($response->isSuccessful()) {
+		    // Parse result
+		    $result = $this->_parseResponse($response);
+		    if (!$result) {
+		        return array();
+		    }
+
+		    $entries = null;
+		    if ($result->entry) {
+    		    if (count($result->entry) > 1) {
+    		        $entries = $result->entry;
+    		    } else {
+    		        $entries = array($result->entry);
+    		    }
+		    } else {
+		        // This one is tricky... If we have properties defined, we have an entity.
+		        $properties = $result->xpath('//m:properties');
+		        if ($properties) {
+		            $entries = array($result);
+		        } else {
+		            return array();
+		        }
+		    }
+
+		    // Create return value
+		    $returnValue = array();		    
+		    foreach ($entries as $entry) {
+    		    // Parse properties
+    		    $properties = $entry->xpath('.//m:properties');
+    		    $properties = $properties[0]->children('http://schemas.microsoft.com/ado/2007/08/dataservices');
+    		    
+    		    // Create entity
+    		    $entity = new $entityClass('', '');
+    		    $entity->setAzureValues((array)$properties, true);
+    		    
+    		    // If we have a Zend_Service_WindowsAzure_Storage_DynamicTableEntity, make sure all property types are OK
+    		    if ($entity instanceof Zend_Service_WindowsAzure_Storage_DynamicTableEntity) {
+    		        foreach ($properties as $key => $value) {  
+    		            $attributes = $value->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
+    		            $type = (string)$attributes['type'];
+    		            if ($type !== '') {
+    		                $entity->setAzurePropertyType($key, $type);
+    		            }
+    		        }
+    		    }
+    
+    		    // Update etag
+    		    $etag      = $entry->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
+    		    $etag      = (string)$etag['etag'];
+    		    $entity->setEtag($etag);
+    		    
+    		    // Add to result
+    		    $returnValue[] = $entity;
+		    }
+
+			// More entities?
+		    if (!is_null($response->getHeader('x-ms-continuation-NextPartitionKey')) && !is_null($response->getHeader('x-ms-continuation-NextRowKey'))) {
+		        if (strpos($queryString, '$top') === false) {
+		            $returnValue = array_merge($returnValue, $this->retrieveEntities($tableName, $filter, $entityClass, $response->getHeader('x-ms-continuation-NextPartitionKey'), $response->getHeader('x-ms-continuation-NextRowKey')));
+		        }
+		    }
+		    
+		    // Return
+		    return $returnValue;
+		} else {
+		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Update entity by replacing it
+	 * 
+	 * @param string                              $tableName   Table name
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
+	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function updateEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
+	{
+	    return $this->_changeEntity(Zend_Http_Client::PUT, $tableName, $entity, $verifyEtag);
+	}
+	
+	/**
+	 * Update entity by adding or updating properties
+	 * 
+	 * @param string                              $tableName   Table name
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
+	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
+	 * @param array                               $properties  Properties to merge. All properties will be used when omitted.
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	public function mergeEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false, $properties = array())
+	{
+		$mergeEntity = null;
+		if (is_array($properties) && count($properties) > 0) {
+			// Build a new object
+			$mergeEntity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
+			
+			// Keep only values mentioned in $properties
+			$azureValues = $entity->getAzureValues();
+			foreach ($azureValues as $key => $value) {
+				if (in_array($value->Name, $properties)) {
+					$mergeEntity->setAzureProperty($value->Name, $value->Value, $value->Type);
+				}
+			}
+		} else {
+			$mergeEntity = $entity;
+		}
+		
+	    return $this->_changeEntity(Zend_Http_Client::MERGE, $tableName, $mergeEntity, $verifyEtag);
+	}
+	
+	/**
+	 * Get error message from Zend_Http_Response
+	 * 
+	 * @param Zend_Http_Response $response Repsonse
+	 * @param string $alternativeError Alternative error message
+	 * @return string
+	 */
+	protected function _getErrorMessage(Zend_Http_Response $response, $alternativeError = 'Unknown error.')
+	{
+		$response = $this->_parseResponse($response);
+		if ($response && $response->message) {
+		    return (string)$response->message;
+		} else {
+		    return $alternativeError;
+		}
+	}
+	
+	/**
+	 * Update entity / merge entity
+	 * 
+	 * @param string                              $httpVerb    HTTP verb to use (PUT = update, MERGE = merge)
+	 * @param string                              $tableName   Table name
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
+	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
+	 * @throws Zend_Service_WindowsAzure_Exception
+	 */
+	protected function _changeEntity($httpVerb = Zend_Http_Client::PUT, $tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
+	{
+		if ($tableName === '') {
+			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
+		}
+		if (is_null($entity)) {
+			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
+		}
+		                     
+        // Add header information
+        $headers = array();
+        $headers['Content-Type']   = 'application/atom+xml';
+        $headers['Content-Length'] = 0;
+        if (!$verifyEtag) {
+            $headers['If-Match']       = '*';
+        } else {
+            $headers['If-Match']       = $entity->getEtag();
+        }
+
+	    // Generate request body
+		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+                        <entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
+                          <title />
+                          <updated>{tpl:Updated}</updated>
+                          <author>
+                            <name />
+                          </author>
+                          <id />
+                          <content type="application/xml">
+                            <m:properties>
+                              {tpl:Properties}
+                            </m:properties>
+                          </content>
+                        </entry>';
+		
+        $requestBody = $this->_fillTemplate($requestBody, array(
+        	'Updated'    => $this->isoDate(),
+            'Properties' => $this->_generateAzureRepresentation($entity)
+        ));
+
+        // Add header information
+        $headers = array();
+        $headers['Content-Type'] = 'application/atom+xml';
+	    if (!$verifyEtag) {
+            $headers['If-Match']       = '*';
+        } else {
+            $headers['If-Match']       = $entity->getEtag();
+        }
+        
+		// Perform request
+		$response = null;
+	    if ($this->isInBatch()) {
+		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
+		    return null;
+		} else {
+		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
+		}
+		if ($response->isSuccessful()) {
+		    // Update properties
+			$entity->setEtag($response->getHeader('Etag'));
+			$entity->setTimestamp($response->getHeader('Last-modified'));
+
+		    return $entity;
+		} else {
+			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
+		}
+	}
+	
+	/**
+	 * Fill text template with variables from key/value array
+	 * 
+	 * @param string $templateText Template text
+	 * @param array $variables Array containing key/value pairs
+	 * @return string
+	 */
+	protected function _fillTemplate($templateText, $variables = array())
+	{
+	    foreach ($variables as $key => $value) {
+	        $templateText = str_replace('{tpl:' . $key . '}', $value, $templateText);
+	    }
+	    return $templateText;
+	}
+	
+	/**
+	 * Generate Azure representation from entity (creates atompub markup from properties)
+	 * 
+	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity
+	 * @return string
+	 */
+	protected function _generateAzureRepresentation(Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
+	{
+		// Generate Azure representation from entity
+		$azureRepresentation = array();
+		$azureValues         = $entity->getAzureValues();
+		foreach ($azureValues as $azureValue) {
+		    $value = array();
+		    $value[] = '<d:' . $azureValue->Name;
+		    if ($azureValue->Type != '') {
+		        $value[] = ' m:type="' . $azureValue->Type . '"';
+		    }
+		    if (is_null($azureValue->Value)) {
+		        $value[] = ' m:null="true"'; 
+		    }
+		    $value[] = '>';
+		    
+		    if (!is_null($azureValue->Value)) {
+		        if (strtolower($azureValue->Type) == 'edm.boolean') {
+		            $value[] = ($azureValue->Value == true ? '1' : '0');
+		        } else {
+		            $value[] = $azureValue->Value;
+		        }
+		    }
+		    
+		    $value[] = '</d:' . $azureValue->Name . '>';
+		    $azureRepresentation[] = implode('', $value);
+		}
+
+		return implode('', $azureRepresentation);
+	}
+}

+ 323 - 0
library/Zend/Service/WindowsAzure/Storage/TableEntity.php

@@ -0,0 +1,323 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobInstance.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_TableEntity
+{
+    /**
+     * Partition key
+     * 
+     * @var string
+     */
+    protected $_partitionKey;
+    
+    /**
+     * Row key
+     * 
+     * @var string
+     */
+    protected $_rowKey;
+    
+    /**
+     * Timestamp
+     * 
+     * @var string
+     */
+    protected $_timestamp = '1900-01-01T00:00:00';
+    
+    /**
+     * Etag
+     * 
+     * @var string
+     */
+    protected $_etag = '';
+    
+    /**
+     * Constructor
+     * 
+     * @param string  $partitionKey    Partition key
+     * @param string  $rowKey          Row key
+     */
+    public function __construct($partitionKey = '', $rowKey = '') 
+    {	        
+        $this->_partitionKey = $partitionKey;
+        $this->_rowKey       = $rowKey;
+    }
+    
+    /**
+     * Get partition key
+     * 
+     * @azure PartitionKey
+     * @return string
+     */
+    public function getPartitionKey()
+    {
+        return $this->_partitionKey;
+    }
+    
+    /**
+     * Set partition key
+     * 
+     * @azure PartitionKey
+     * @param string $value
+     */
+    public function setPartitionKey($value)
+    {
+        $this->_partitionKey = $value;
+    }
+    
+    /**
+     * Get row key
+     * 
+     * @azure RowKey
+     * @return string
+     */
+    public function getRowKey()
+    {
+        return $this->_rowKey;
+    }
+    
+    /**
+     * Set row key
+     * 
+     * @azure RowKey
+     * @param string $value
+     */
+    public function setRowKey($value)
+    {
+        $this->_rowKey = $value;
+    }
+    
+    /**
+     * Get timestamp
+     * 
+     * @azure Timestamp Edm.DateTime
+     * @return string
+     */
+    public function getTimestamp()
+    {
+        return $this->_timestamp;
+    }
+    
+    /**
+     * Set timestamp
+     * 
+     * @azure Timestamp Edm.DateTime
+     * @param string $value
+     */
+    public function setTimestamp($value = '1900-01-01T00:00:00')
+    {
+        $this->_timestamp = $value;
+    }
+    
+    /**
+     * Get etag
+     * 
+     * @return string
+     */
+    public function getEtag()
+    {
+        return $this->_etag;
+    }
+    
+    /**
+     * Set etag
+     * 
+     * @param string $value
+     */
+    public function setEtag($value = '')
+    {
+        $this->_etag = $value;
+    }
+    
+    /**
+     * Get Azure values
+     * 
+     * @return array
+     */
+    public function getAzureValues()
+    {
+        // Get accessors
+        $accessors = self::getAzureAccessors(get_class($this));
+        
+        // Loop accessors and retrieve values
+        $returnValue = array();
+        foreach ($accessors as $accessor) {
+            if ($accessor->EntityType == 'ReflectionProperty') {
+                $property = $accessor->EntityAccessor;
+                $returnValue[] = (object)array(
+                    'Name'  => $accessor->AzurePropertyName,
+                	'Type'  => $accessor->AzurePropertyType,
+                	'Value' => $this->$property,
+                );
+            } else if ($accessor->EntityType == 'ReflectionMethod' && substr(strtolower($accessor->EntityAccessor), 0, 3) == 'get') {
+                $method = $accessor->EntityAccessor;
+                $returnValue[] = (object)array(
+                    'Name'  => $accessor->AzurePropertyName,
+                	'Type'  => $accessor->AzurePropertyType,
+                	'Value' => $this->$method(),
+                );
+            }
+        }
+        
+        // Return
+        return $returnValue;
+    }
+    
+    /**
+     * Set Azure values
+     * 
+     * @param array $values
+     * @param boolean $throwOnError Throw Zend_Service_WindowsAzure_Exception when a property is not specified in $values?
+     * @throws Zend_Service_WindowsAzure_Exception
+     */
+    public function setAzureValues($values = array(), $throwOnError = false)
+    {
+        // Get accessors
+        $accessors = self::getAzureAccessors(get_class($this));
+        
+        // Loop accessors and set values
+        $returnValue = array();
+        foreach ($accessors as $accessor) {
+            if (isset($values[$accessor->AzurePropertyName])) {
+                // Cast to correct type
+                if ($accessor->AzurePropertyType != '') {
+                    switch (strtolower($accessor->AzurePropertyType)) {
+        	            case 'edm.int32':
+        	            case 'edm.int64':
+        	                $values[$accessor->AzurePropertyName] = intval($values[$accessor->AzurePropertyName]); break;
+        	            case 'edm.boolean':
+        	                if ($values[$accessor->AzurePropertyName] == 'true' || $values[$accessor->AzurePropertyName] == '1')
+        	                    $values[$accessor->AzurePropertyName] = true;
+        	                else
+        	                    $values[$accessor->AzurePropertyName] = false;
+        	                break;
+        	            case 'edm.double':
+        	                $values[$accessor->AzurePropertyName] = floatval($values[$accessor->AzurePropertyName]); break;
+        	        }
+                }
+                
+                // Assign value
+                if ($accessor->EntityType == 'ReflectionProperty') {
+                    $property = $accessor->EntityAccessor;
+                    $this->$property = $values[$accessor->AzurePropertyName];
+                } else if ($accessor->EntityType == 'ReflectionMethod' && substr(strtolower($accessor->EntityAccessor), 0, 3) == 'set') {
+                    $method = $accessor->EntityAccessor;
+                    $this->$method($values[$accessor->AzurePropertyName]);
+                }
+            } else if ($throwOnError) {
+                throw new Zend_Service_WindowsAzure_Exception("Property '" . $accessor->AzurePropertyName . "' was not found in \$values array");    
+            }
+        }
+        
+        // Return
+        return $returnValue;
+    }
+    
+    /**
+     * Get Azure accessors from class
+     * 
+     * @param string $className Class to get accessors for
+     * @return array
+     */
+    public static function getAzureAccessors($className = '')
+    {
+        // List of accessors
+        $azureAccessors = array();
+        
+        // Get all types
+        $type = new ReflectionClass($className);
+        
+        // Loop all properties
+        $properties = $type->getProperties();
+        foreach ($properties as $property) {
+            $accessor = self::getAzureAccessor($property);
+            if (!is_null($accessor)) {
+                $azureAccessors[] = $accessor;
+            }
+        }
+        
+        // Loop all methods
+        $methods = $type->getMethods();
+        foreach ($methods as $method) {
+            $accessor = self::getAzureAccessor($method);
+            if (!is_null($accessor)) {
+                $azureAccessors[] = $accessor;
+            }
+        }
+        
+        // Return
+        return $azureAccessors;
+    }
+    
+    /**
+     * Get Azure accessor from reflection member
+     * 
+     * @param ReflectionProperty|ReflectionMethod $member
+     * @return object
+     */
+    public static function getAzureAccessor($member)
+    {
+        // Get comment
+        $docComment = $member->getDocComment();
+        
+        // Check for Azure comment
+        if (strpos($docComment, '@azure') === false)
+        {
+            return null;
+        }
+            
+        // Search for @azure contents
+        $azureComment = '';
+        $commentLines = explode("\n", $docComment);
+        foreach ($commentLines as $commentLine) {
+            if (strpos($commentLine, '@azure') !== false) {
+                $azureComment = trim(substr($commentLine, strpos($commentLine, '@azure') + 6));
+                while (strpos($azureComment, '  ') !== false) {
+                    $azureComment = str_replace('  ', ' ', $azureComment);
+                }
+                break;
+            }
+        }
+        
+        // Fetch @azure properties
+        $azureProperties = explode(' ', $azureComment);
+        return (object)array(
+            'EntityAccessor'    => $member->getName(),
+            'EntityType'        => get_class($member),
+            'AzurePropertyName' => $azureProperties[0],
+        	'AzurePropertyType' => isset($azureProperties[1]) ? $azureProperties[1] : ''
+        );
+    }
+}

+ 326 - 0
library/Zend/Service/WindowsAzure/Storage/TableEntityQuery.php

@@ -0,0 +1,326 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Blob.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Storage_TableEntityQuery
+{
+    /**
+     * From
+     * 
+     * @var string
+     */
+	protected $_from  = '';
+	
+	/**
+	 * Where
+	 * 
+	 * @var array
+	 */
+	protected $_where = array();
+	
+	/**
+	 * Order by
+	 * 
+	 * @var array
+	 */
+	protected $_orderBy = array();
+	
+	/**
+	 * Top
+	 * 
+	 * @var int
+	 */
+	protected $_top = null;
+	
+	/**
+	 * Partition key
+	 * 
+	 * @var string
+	 */
+	protected $_partitionKey = null;
+
+	/**
+	 * Row key
+	 * 
+	 * @var string
+	 */
+	protected $_rowKey = null;
+	
+	/**
+	 * Select clause
+	 * 
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function select()
+	{
+		return $this;
+	}
+	
+	/**
+	 * From clause
+	 * 
+	 * @param string $name Table name to select entities from
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function from($name)
+	{
+		$this->_from = $name;
+		return $this;
+	}
+	
+	/**
+	 * Specify partition key
+	 * 
+	 * @param string $value Partition key to query for
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function wherePartitionKey($value = null)
+	{
+	    $this->_partitionKey = $value;
+	    return $this;
+	}
+	
+	/**
+	 * Specify row key
+	 * 
+	 * @param string $value Row key to query for
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function whereRowKey($value = null)
+	{
+	    $this->_rowKey = $value;
+	    return $this;
+	}
+	
+	/**
+	 * Add where clause
+	 * 
+	 * @param string       $condition   Condition, can contain question mark(s) (?) for parameter insertion.
+	 * @param string|array $value       Value(s) to insert in question mark (?) parameters.
+	 * @param string       $cond        Condition for the clause (and/or/not)
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function where($condition, $value = null, $cond = '')
+	{
+	    $condition = $this->_replaceOperators($condition);
+	    
+	    if (!is_null($value)) {
+	        $condition = $this->_quoteInto($condition, $value);
+	    }
+	    
+		if (count($this->_where) == 0) {
+			$cond = '';
+		} else if ($cond !== '') {
+			$cond = ' ' . strtolower(trim($cond)) . ' ';
+		}
+		
+		$this->_where[] = $cond . $condition;
+		return $this;
+	}
+
+	/**
+	 * Add where clause with AND condition
+	 * 
+	 * @param string       $condition   Condition, can contain question mark(s) (?) for parameter insertion.
+	 * @param string|array $value       Value(s) to insert in question mark (?) parameters.
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function andWhere($condition, $value = null)
+	{
+		return $this->where($condition, $value, 'and');
+	}
+	
+	/**
+	 * Add where clause with OR condition
+	 * 
+	 * @param string       $condition   Condition, can contain question mark(s) (?) for parameter insertion.
+	 * @param string|array $value       Value(s) to insert in question mark (?) parameters.
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function orWhere($condition, $value = null)
+	{
+		return $this->where($condition, $value, 'or');
+	}
+	
+	/**
+	 * OrderBy clause
+	 * 
+	 * @param string $column    Column to sort by
+	 * @param string $direction Direction to sort (asc/desc)
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+	public function orderBy($column, $direction = 'asc')
+	{
+		$this->_orderBy[] = $column . ' ' . $direction;
+		return $this;
+	}
+    
+	/**
+	 * Top clause
+	 * 
+	 * @param int $top  Top to fetch
+	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
+	 */
+    public function top($top = null)
+    {
+        $this->_top  = (int)$top;
+        return $this;
+    }
+	
+    /**
+     * Assembles the query string
+     * 
+     * @param boolean $urlEncode Apply URL encoding to the query string
+     * @return string
+     */
+	public function assembleQueryString($urlEncode = false)
+	{
+		$query = array();
+		if (count($this->_where) != 0) {
+		    $filter = implode('', $this->_where);
+			$query[] = '$filter=' . ($urlEncode ? urlencode($filter) : $filter);
+		}
+		
+		if (count($this->_orderBy) != 0) {
+		    $orderBy = implode(',', $this->_orderBy);
+			$query[] = '$orderby=' . ($urlEncode ? urlencode($orderBy) : $orderBy);
+		}
+		
+		if (!is_null($this->_top)) {
+			$query[] = '$top=' . $this->_top;
+		}
+		
+		if (count($query) != 0) {
+			return '?' . implode('&', $query);
+		}
+		
+		return '';
+	}
+	
+	/**
+	 * Assemble from
+	 * 
+	 * @param boolean $includeParentheses Include parentheses? ()
+	 * @return string
+	 */
+	public function assembleFrom($includeParentheses = true)
+	{
+	    $identifier = '';
+	    if ($includeParentheses) {
+	        $identifier .= '(';
+	        
+	        if (!is_null($this->_partitionKey)) {
+	            $identifier .= 'PartitionKey=\'' . $this->_partitionKey . '\'';
+	        }
+	            
+	        if (!is_null($this->_partitionKey) && !is_null($this->_rowKey)) {
+	            $identifier .= ', ';
+	        }
+	            
+	        if (!is_null($this->_rowKey)) {
+	            $identifier .= 'RowKey=\'' . $this->_rowKey . '\'';
+	        }
+	            
+	        $identifier .= ')';
+	    }
+		return $this->_from . $identifier;
+	}
+	
+	/**
+	 * Assemble full query
+	 * 
+	 * @return string
+	 */
+	public function assembleQuery()
+	{
+		$assembledQuery = $this->assembleFrom();
+		
+		$queryString = $this->assembleQueryString();
+		if ($queryString !== '') {
+			$assembledQuery .= $queryString;
+		}
+		
+		return $assembledQuery;
+	}
+	
+	/**
+	 * Quotes a variable into a condition
+	 * 
+	 * @param string       $text   Condition, can contain question mark(s) (?) for parameter insertion.
+	 * @param string|array $value  Value(s) to insert in question mark (?) parameters.
+	 * @return string
+	 */
+	protected function _quoteInto($text, $value = null)
+	{
+		if (!is_array($value)) {
+	        $text = str_replace('?', '\'' . addslashes($value) . '\'', $text);
+	    } else {
+	        $i = 0;
+	        while(strpos($text, '?') !== false) {
+	            if (is_numeric($value[$i])) {
+	                $text = substr_replace($text, $value[$i++], strpos($text, '?'), 1);
+	            } else {
+	                $text = substr_replace($text, '\'' . addslashes($value[$i++]) . '\'', strpos($text, '?'), 1);
+	            }
+	        }
+	    }
+	    return $text;
+	}
+	
+	/**
+	 * Replace operators
+	 * 
+	 * @param string $text
+	 * @return string
+	 */
+	protected function _replaceOperators($text)
+	{
+	    $text = str_replace('==', 'eq',  $text);
+	    $text = str_replace('>',  'gt',  $text);
+	    $text = str_replace('<',  'lt',  $text);
+	    $text = str_replace('>=', 'ge',  $text);
+	    $text = str_replace('<=', 'le',  $text);
+	    $text = str_replace('!=', 'ne',  $text);
+	    
+	    $text = str_replace('&&', 'and', $text);
+	    $text = str_replace('||', 'or',  $text);
+	    $text = str_replace('!',  'not', $text);
+	    
+	    return $text;
+	}
+	
+	/**
+	 * __toString overload
+	 * 
+	 * @return string
+	 */
+	public function __toString()
+	{
+		return $this->assembleQuery();
+	}
+}

+ 95 - 0
library/Zend/Service/WindowsAzure/Storage/TableInstance.php

@@ -0,0 +1,95 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: BlobInstance.php 14561 2009-05-07 08:05:12Z unknown $
+ */
+
+/**
+ * @see Zend_Service_WindowsAzure_Exception
+ */
+require_once 'Zend/Service/WindowsAzure/Exception.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage Storage
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * 
+ * @property string  $Id              Id
+ * @property string  $Name            Name
+ * @property string  $Href            Href
+ * @property string  $Updated         Updated
+ */
+class Zend_Service_WindowsAzure_Storage_TableInstance
+{
+    /**
+     * Data
+     * 
+     * @var array
+     */
+    protected $_data = null;
+    
+    /**
+     * Constructor
+     * 
+     * @param string  $id              Id
+     * @param string  $name            Name
+     * @param string  $href            Href
+     * @param string  $updated         Updated
+     */
+    public function __construct($id, $name, $href, $updated) 
+    {	        
+        $this->_data = array(
+            'id'               => $id,
+            'name'             => $name,
+            'href'             => $href,
+            'updated'          => $updated
+        );
+    }
+    
+    /**
+     * Magic overload for setting properties
+     * 
+     * @param string $name     Name of the property
+     * @param string $value    Value to set
+     */
+    public function __set($name, $value) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            $this->_data[strtolower($name)] = $value;
+            return;
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+
+    /**
+     * Magic overload for getting properties
+     * 
+     * @param string $name     Name of the property
+     */
+    public function __get($name) {
+        if (array_key_exists(strtolower($name), $this->_data)) {
+            return $this->_data[strtolower($name)];
+        }
+
+        throw new Exception("Unknown property: " . $name);
+    }
+}

+ 79 - 19
tests/TestConfiguration.php.dist

@@ -562,6 +562,27 @@ define('TESTS_ZEND_SERVICE_FLICKR_ONLINE_ENABLED', false);
 define('TESTS_ZEND_SERVICE_FLICKR_ONLINE_APIKEY', 'Enter API key here');
 
 /**
+ * Zend_Service_LiveDocx configuration
+ *
+ * Define username and password in order to run unit tests for LiveDocX web 
+ * services.
+ *
+ * phpunit/phpunit will typically work.
+ */
+define('TESTS_ZEND_SERVICE_LIVEDOCX_USERNAME', false);
+define('TESTS_ZEND_SERVICE_LIVEDOCX_PASSWORD', false);
+
+/**
+ * Zend_Service_ReCaptcha tests
+ */
+define('TESTS_ZEND_SERVICE_RECAPTCHA_ENABLED', false);
+define('TESTS_ZEND_SERVICE_RECAPTCHA_ONLINE_ENABLED', false);
+define('TESTS_ZEND_SERVICE_RECAPTCHA_PUBLIC_KEY', 'public key');
+define('TESTS_ZEND_SERVICE_RECAPTCHA_PRIVATE_KEY', 'private key');
+define('TESTS_ZEND_SERVICE_RECAPTCHA_MAILHIDE_PUBLIC_KEY', 'public mailhide key');
+define('TESTS_ZEND_SERVICE_RECAPTCHA_MAILHIDE_PRIVATE_KEY', 'private mailhide key');
+
+/**
  * Zend_Service_Simpy tests
  */
 define('TESTS_ZEND_SERVICE_SIMPY_ENABLED', false);
@@ -598,33 +619,72 @@ define('TESTS_ZEND_SERVICE_TWITTER_ONLINE_ENABLED', false);
 define('TESTS_ZEND_SERVICE_TWITTER_USER', 'zftestuser');
 define('TESTS_ZEND_SERVICE_TWITTER_PASS', 'zftestuser');
 
+/**
+ * Zend_Service_WindowsAzure  tests
+ */
 
 /**
- * Zend_Service_Yahoo online tests
+ * Proxy settings
  */
-define('TESTS_ZEND_SERVICE_YAHOO_ONLINE_ENABLED', false);
-define('TESTS_ZEND_SERVICE_YAHOO_ONLINE_APPID', 'Enter APPID here');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY',                false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY',                   '');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT',              '8080');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS',       '');
 
 /**
- * Zend_Service_ReCaptcha tests
+ * Azure hosts
  */
-define('TESTS_ZEND_SERVICE_RECAPTCHA_ENABLED', false);
-define('TESTS_ZEND_SERVICE_RECAPTCHA_ONLINE_ENABLED', false);
-define('TESTS_ZEND_SERVICE_RECAPTCHA_PUBLIC_KEY', 'public key');
-define('TESTS_ZEND_SERVICE_RECAPTCHA_PRIVATE_KEY', 'private key');
-define('TESTS_ZEND_SERVICE_RECAPTCHA_MAILHIDE_PUBLIC_KEY', 'public mailhide key');
-define('TESTS_ZEND_SERVICE_RECAPTCHA_MAILHIDE_PRIVATE_KEY', 'private mailhide key');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_DEV',                   '127.0.0.1:10000');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_HOST_DEV',                  '127.0.0.1:10001');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_DEV',                  '127.0.0.1:10002');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_PROD',                  'blob.core.windows.net');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_HOST_PROD',                 'queue.core.windows.net');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_PROD',                 'table.core.windows.net');
 
 /**
- * Zend_Service_LiveDocx configuration
- *
- * Define username and password in order to run unit tests for LiveDocX web 
- * services.
- *
- * phpunit/phpunit will typically work.
+ * Credentials
  */
-define('TESTS_ZEND_SERVICE_LIVEDOCX_USERNAME', false);
-define('TESTS_ZEND_SERVICE_LIVEDOCX_PASSWORD', false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV',             'devstoreaccount1');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV',                 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD',            'phpazure');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD',                'I+ebYPcIDB6BsmfAe6pJSpOw8oXA6jMBZv1BEZcSPRqTpldt44refCl65YpKJqcBOiD21Lxsj8d6Ah8Oc2/gKA==');
+
+/**
+ * Blob storage tests
+ */
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS',                   true);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNONPROD',                  false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNLARGEBLOB',               true);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_CONTAINER_PREFIX',           'phpazuretestblob');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSTREAM_CONTAINER_PREFIX',     'phpazureteststream');
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSA_CONTAINER_PREFIX',         'phpazuretestshared');
+
+/**
+ * Table storage tests
+ */
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS',                  true);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNONPROD',                 false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_TABLENAME_PREFIX',          'phpazuretesttable');
+
+/**
+ * Queue storage tests
+ */
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS',                  true);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNONPROD',                 false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_PREFIX',                    'phpazuretestqueue');
+
+/**
+ * SessionHandler tests
+ */
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS',         true);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNONPROD',        false);
+define('TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_TABLENAME_PREFIX', 'phpazuretestsession');
+
+/**
+ * Zend_Service_Yahoo online tests
+ */
+define('TESTS_ZEND_SERVICE_YAHOO_ONLINE_ENABLED', false);
+define('TESTS_ZEND_SERVICE_YAHOO_ONLINE_APPID', 'Enter APPID here');
 
 /**
  * Zend_Soap_AutoDiscover scenario tests for complex objects and wsdl generation
@@ -662,4 +722,4 @@ define('TESTS_ZEND_VALIDATE_ONLINE_ENABLED', false);
  * PHPUnit Code Coverage / Test Report
  */
 define('TESTS_GENERATE_REPORT', false);
-define('TESTS_GENERATE_REPORT_TARGET', '/path/to/target');
+define('TESTS_GENERATE_REPORT_TARGET', '/path/to/target');

+ 2 - 0
tests/Zend/Service/AllTests.php

@@ -40,6 +40,7 @@ require_once 'Zend/Service/StrikeIron/AllTests.php';
 require_once 'Zend/Service/Technorati/AllTests.php';
 require_once 'Zend/Service/TwitterTest.php';
 require_once 'Zend/Service/TwitterSearchTest.php';
+require_once 'Zend/Service/WindowsAzure/AllTests.php';
 require_once 'Zend/Service/Yahoo/AllTests.php';
 
 /**
@@ -85,6 +86,7 @@ class Zend_Service_AllTests
         $suite->addTest(Zend_Service_Technorati_AllTests::suite());
         $suite->addTestSuite('Zend_Service_TwitterTest');
         $suite->addTestSuite('Zend_Service_TwitterSearchTest');
+        $suite->addTest(Zend_Service_WindowsAzure_AllTests::suite());
         $suite->addTest(Zend_Service_Yahoo_AllTests::suite());
 
         return $suite;

+ 91 - 0
tests/Zend/Service/WindowsAzure/AllTests.php

@@ -0,0 +1,91 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: AllTests.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_AllTests::main');
+}
+
+require_once 'Zend/Service/WindowsAzure/Credentials/AllTests.php';
+require_once 'Zend/Service/WindowsAzure/RetryPolicyTest.php';
+require_once 'Zend/Service/WindowsAzure/StorageTest.php';
+require_once 'Zend/Service/WindowsAzure/BlobStorageTest.php';
+require_once 'Zend/Service/WindowsAzure/BlobStreamTest.php';
+require_once 'Zend/Service/WindowsAzure/BlobStorageSharedAccessTest.php';
+require_once 'Zend/Service/WindowsAzure/TableEntityTest.php';
+require_once 'Zend/Service/WindowsAzure/DynamicTableEntityTest.php';
+require_once 'Zend/Service/WindowsAzure/TableEntityQueryTest.php';
+require_once 'Zend/Service/WindowsAzure/TableStorageTest.php';
+require_once 'Zend/Service/WindowsAzure/QueueStorageTest.php';
+require_once 'Zend/Service/WindowsAzure/SessionHandlerTest.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: AllTests.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Service_WindowsAzure');
+
+
+        $suite->addTest(Zend_Service_WindowsAzure_Credentials_AllTests::suite());
+        $suite->addTestSuite('Zend_Service_WindowsAzure_RetryPolicyTest');
+        $suite->addTestSuite('Zend_Service_WindowsAzure_StorageTest');
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $suite->addTestSuite('Zend_Service_WindowsAzure_BlobStorageTest');
+            $suite->addTestSuite('Zend_Service_WindowsAzure_BlobStorageSharedAccessTest');
+            $suite->addTestSuite('Zend_Service_WindowsAzure_BlobStreamTest');
+        }
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $suite->addTestSuite('Zend_Service_WindowsAzure_TableEntityTest');
+            $suite->addTestSuite('Zend_Service_WindowsAzure_DynamicTableEntityTest');
+            $suite->addTestSuite('Zend_Service_WindowsAzure_TableEntityQueryTest');
+            $suite->addTestSuite('Zend_Service_WindowsAzure_TableStorageTest');
+        }
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $suite->addTestSuite('Zend_Service_WindowsAzure_QueueStorageTest');
+        }
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $suite->addTestSuite('Zend_Service_WindowsAzure_SessionHandlerTest');
+        }
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Service_WindowsAzure_AllTests::main') {
+    Zend_Service_WindowsAzure_AllTests::main();
+}

+ 223 - 0
tests/Zend/Service/WindowsAzure/BlobStorageSharedAccessTest.php

@@ -0,0 +1,223 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageSharedAccessTest.php 25258 2009-08-14 08:40:41Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_BlobStorageSharedAccessTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_Blob */
+require_once 'Zend/Service/WindowsAzure/Storage/Blob.php';
+
+/** Zend_Service_WindowsAzure_Credentials_SharedAccessSignature */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedAccessSignature.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageSharedAccessTest.php 25258 2009-08-14 08:40:41Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_BlobStorageSharedAccessTest extends PHPUnit_Framework_TestCase
+{
+    static $path;
+    
+    public function __construct()
+    {
+        self::$path = dirname(__FILE__).'/_files/';
+    }
+    
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_BlobStorageSharedAccessTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+   
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createAdministrativeStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteContainer(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSA_CONTAINER_PREFIX . $i); } catch (Exception $e) { }
+        }
+        try { $storageClient->deleteContainer('$root'); } catch (Exception $e) { }
+    }
+
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+            $storageClient->setCredentials(
+                new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false)
+            );
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+            $storageClient->setCredentials(
+                new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true)
+            );
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected function createAdministrativeStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSA_CONTAINER_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test shared access, only write
+     */
+    public function testSharedAccess_OnlyWrite()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            
+            // Account owner performs this part
+            $administrativeStorageClient = $this->createAdministrativeStorageInstance();
+            $administrativeStorageClient->createContainer($containerName);
+            
+            $sharedAccessUrl = $administrativeStorageClient->generateSharedAccessUrl(
+                $containerName,
+                '',
+            	'c', 
+            	'w',
+            	$administrativeStorageClient->isoDate(time() - 500),
+            	$administrativeStorageClient->isoDate(time() + 3000)
+            );
+
+            
+            // Reduced permissions user performs this part
+            $storageClient = $this->createStorageInstance();
+            $credentials = $storageClient->getCredentials();
+            $credentials->setPermissionSet(array(
+                $sharedAccessUrl
+            ));
+
+            $result = $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+    
+            $this->assertEquals($containerName, $result->Container);
+            $this->assertEquals('images/WindowsAzure.gif', $result->Name);
+            
+            
+            
+            // Now make sure reduced permissions user can not view the uploaded blob
+            $exceptionThrown = false;
+            try {
+                $storageClient->getBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+    
+    /**
+     * Test different accounts
+     */
+    public function testDifferentAccounts()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            
+            // Account owner performs this part
+            $administrativeStorageClient = $this->createAdministrativeStorageInstance();
+            $administrativeStorageClient->createContainer($containerName);
+            
+            $sharedAccessUrl1 = $administrativeStorageClient->generateSharedAccessUrl(
+                $containerName,
+                '',
+            	'c', 
+            	'w',
+            	$administrativeStorageClient->isoDate(time() - 500),
+            	$administrativeStorageClient->isoDate(time() + 3000)
+            );
+            $sharedAccessUrl2 = str_replace($administrativeStorageClient->getAccountName(), 'bogusaccount', $sharedAccessUrl1);
+
+            
+            // Reduced permissions user performs this part and should fail,
+            // because different accounts have been used
+            $storageClient = $this->createStorageInstance();
+            $credentials = $storageClient->getCredentials();
+
+            $exceptionThrown = false;
+            try {
+	            $credentials->setPermissionSet(array(
+	                $sharedAccessUrl1,
+	                $sharedAccessUrl2
+	            ));
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+}
+
+// Call Zend_Service_WindowsAzure_BlobStorageSharedAccessTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_BlobStorageSharedAccessTest::main") {
+    Zend_Service_WindowsAzure_BlobStorageSharedAccessTest::main();
+}

+ 514 - 0
tests/Zend/Service/WindowsAzure/BlobStorageTest.php

@@ -0,0 +1,514 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_BlobStorageTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_Blob */
+require_once 'Zend/Service/WindowsAzure/Storage/Blob.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_BlobStorageTest extends PHPUnit_Framework_TestCase
+{
+    static $path;
+    
+    public function __construct()
+    {
+        self::$path = dirname(__FILE__).'/_files/';
+    }
+    
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_BlobStorageTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+   
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteContainer(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_CONTAINER_PREFIX . $i); } catch (Exception $e) { }
+        }
+        try { $storageClient->deleteContainer('$root'); } catch (Exception $e) { }
+    }
+
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNONPROD)
+        {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_CONTAINER_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test container exists
+     */
+    public function testContainerExists()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName1 = $this->generateName();
+            $containerName2 = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName1);
+            $storageClient->createContainer($containerName2);
+            
+            $result = $storageClient->containerExists($containerName1);
+            $this->assertTrue($result);
+            
+            $result = $storageClient->containerExists(md5(time()));
+            $this->assertFalse($result);
+        }
+    }
+    
+    /**
+     * Test blob exists
+     */
+    public function testBlobExists()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $storageClient->putBlob($containerName, 'WindowsAzure1.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'WindowsAzure2.gif', self::$path . 'WindowsAzure.gif');
+            
+            $result = $storageClient->blobExists($containerName, 'WindowsAzure1.gif');
+            $this->assertTrue($result);
+            
+            $result = $storageClient->blobExists($containerName, md5(time()));
+            $this->assertFalse($result);
+        }
+    }
+
+    /**
+     * Test create container
+     */
+    public function testCreateContainer()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $result = $storageClient->createContainer($containerName);
+            $this->assertEquals($containerName, $result->Name);
+        }
+    }
+    
+    /**
+     * Test get container acl
+     */
+    public function testGetContainerAcl()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $acl = $storageClient->getContainerAcl($containerName);
+            $this->assertEquals(Zend_Service_WindowsAzure_Storage_Blob::ACL_PRIVATE, $acl);        
+        }
+    }
+    
+    /**
+     * Test set container acl
+     */
+    public function testSetContainerAcl()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->setContainerAcl($containerName, Zend_Service_WindowsAzure_Storage_Blob::ACL_PUBLIC);
+            $acl = $storageClient->getContainerAcl($containerName);
+            
+            $this->assertEquals(Zend_Service_WindowsAzure_Storage_Blob::ACL_PUBLIC, $acl);
+        }
+    }
+    
+    /**
+     * Test set container acl advanced
+     */
+    public function testSetContainerAclAdvanced()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->setContainerAcl(
+                $containerName,
+                Zend_Service_WindowsAzure_Storage_Blob::ACL_PRIVATE,
+                array(
+                    new Zend_Service_WindowsAzure_Storage_SignedIdentifier('ABCDEF', '2009-10-10', '2009-10-11', 'r')
+                )
+            );
+            $acl = $storageClient->getContainerAcl($containerName, true);
+            
+            $this->assertEquals(1, count($acl));
+        }
+    }
+
+    /**
+     * Test set container metadata
+     */
+    public function testSetContainerMetadata()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->setContainerMetadata($containerName, array(
+                'createdby' => 'PHPAzure',
+            ));
+            
+            $metadata = $storageClient->getContainerMetadata($containerName);
+            $this->assertEquals('PHPAzure', $metadata['createdby']);
+        }
+    }
+    
+    /**
+     * Test list containers
+     */
+    public function testListContainers()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName1 = 'testlist1';
+            $containerName2 = 'testlist2';
+            $containerName3 = 'testlist3';
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName1);
+            $storageClient->createContainer($containerName2);
+            $storageClient->createContainer($containerName3);
+            $result1 = $storageClient->listContainers('testlist');
+            $result2 = $storageClient->listContainers('testlist', 1);
+    
+            // cleanup first
+            $storageClient->deleteContainer($containerName1);
+            $storageClient->deleteContainer($containerName2);
+            $storageClient->deleteContainer($containerName3);
+            
+            $this->assertEquals(3, count($result1));
+            $this->assertEquals($containerName2, $result1[1]->Name);
+            
+            $this->assertEquals(1, count($result2));
+        }
+    }
+    
+    /**
+     * Test put blob
+     */
+    public function testPutBlob()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $result = $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+    
+            $this->assertEquals($containerName, $result->Container);
+            $this->assertEquals('images/WindowsAzure.gif', $result->Name);
+        }
+    }
+    
+    /**
+     * Test put large blob
+     */
+    public function testPutLargeBlob()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS && TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNLARGEBLOB) {
+            // Create a file > Zend_Service_WindowsAzure_Storage_Blob::MAX_BLOB_SIZE
+            $fileName = $this->_createLargeFile();
+            
+            // Execute test
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $result = $storageClient->putLargeBlob($containerName, 'LargeFile.txt', $fileName);
+    
+            $this->assertEquals($containerName, $result->Container);
+            $this->assertEquals('LargeFile.txt', $result->Name);
+            
+            // Get block list
+            $blockList = $storageClient->getBlockList($containerName, 'LargeFile.txt');
+            $this->assertTrue(count($blockList['CommittedBlocks']) > 0);
+            
+            // Remove file
+            unlink($fileName);
+        }
+    }
+    
+    /**
+     * Test get blob
+     */
+    public function testGetBlob()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+            
+            $fileName = tempnam('', 'tst');
+            $storageClient->getBlob($containerName, 'images/WindowsAzure.gif', $fileName);
+    
+            $this->assertTrue(file_exists($fileName));
+            $this->assertEquals(
+                file_get_contents(self::$path . 'WindowsAzure.gif'),
+                file_get_contents($fileName)
+            );
+            
+            // Remove file
+            unlink($fileName);
+        }
+    }
+    
+    /**
+     * Test set blob metadata
+     */
+    public function testSetBlobMetadata()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+            
+            $storageClient->setBlobMetadata($containerName, 'images/WindowsAzure.gif', array(
+                'createdby' => 'PHPAzure',
+            ));
+            
+            $metadata = $storageClient->getBlobMetadata($containerName, 'images/WindowsAzure.gif');
+            $this->assertEquals('PHPAzure', $metadata['createdby']);
+        }
+    }
+    
+    /**
+     * Test delete blob
+     */
+    public function testDeleteBlob()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->deleteBlob($containerName, 'images/WindowsAzure.gif');
+            
+            $result = $storageClient->listBlobs($containerName);
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Test list blobs
+     */
+    public function testListBlobs()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->putBlob($containerName, 'images/WindowsAzure1.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure2.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure3.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure4.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure5.gif', self::$path . 'WindowsAzure.gif');
+            
+            $result1 = $storageClient->listBlobs($containerName);
+            $this->assertEquals(5, count($result1));
+            $this->assertEquals('images/WindowsAzure5.gif', $result1[4]->Name);
+            
+            $result2 = $storageClient->listBlobs($containerName, '', '', 2);
+            $this->assertEquals(2, count($result2));
+        }
+    }
+    
+    /**
+     * Test copy blob
+     */
+    public function testCopyBlob()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            $source = $storageClient->putBlob($containerName, 'images/WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+    
+            $this->assertEquals($containerName, $source->Container);
+            $this->assertEquals('images/WindowsAzure.gif', $source->Name);
+            
+            $destination = $storageClient->copyBlob($containerName, 'images/WindowsAzure.gif', $containerName, 'images/WindowsAzureCopy.gif');
+    
+            $this->assertEquals($containerName, $destination->Container);
+            $this->assertEquals('images/WindowsAzureCopy.gif', $destination->Name);
+        }
+    }
+    
+    /**
+     * Test root container
+     */
+    public function testRootContainer()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = '$root';
+            $storageClient = $this->createStorageInstance();
+            $result = $storageClient->createContainer($containerName);
+            $this->assertEquals($containerName, $result->Name);
+            
+            // ACL
+            $storageClient->setContainerAcl($containerName, Zend_Service_WindowsAzure_Storage_Blob::ACL_PUBLIC);
+            $acl = $storageClient->getContainerAcl($containerName);
+            
+            $this->assertEquals(Zend_Service_WindowsAzure_Storage_Blob::ACL_PUBLIC, $acl);
+            
+            // Metadata
+            $storageClient->setContainerMetadata($containerName, array(
+                'createdby' => 'PHPAzure',
+            ));
+            
+            $metadata = $storageClient->getContainerMetadata($containerName);
+            $this->assertEquals('PHPAzure', $metadata['createdby']);
+            
+            // List
+            $result = $storageClient->listContainers();
+            $this->assertEquals(1, count($result));
+            
+            // Put blob
+            $result = $storageClient->putBlob($containerName, 'WindowsAzure.gif', self::$path . 'WindowsAzure.gif');
+   
+            $this->assertEquals($containerName, $result->Container);
+            $this->assertEquals('WindowsAzure.gif', $result->Name);
+            
+            // Get blob
+            $fileName = tempnam('', 'tst');
+            $storageClient->getBlob($containerName, 'WindowsAzure.gif', $fileName);
+    
+            $this->assertTrue(file_exists($fileName));
+            $this->assertEquals(
+                file_get_contents(self::$path . 'WindowsAzure.gif'),
+                file_get_contents($fileName)
+            );
+            
+            // Remove file
+            unlink($fileName);
+            
+            // Blob metadata
+            $storageClient->setBlobMetadata($containerName, 'WindowsAzure.gif', array(
+                'createdby' => 'PHPAzure',
+            ));
+            
+            $metadata = $storageClient->getBlobMetadata($containerName, 'WindowsAzure.gif');
+            $this->assertEquals('PHPAzure', $metadata['createdby']);
+            
+            // List blobs
+            $result = $storageClient->listBlobs($containerName);
+            $this->assertEquals(1, count($result));
+            
+            // Delete blob
+            $storageClient->deleteBlob($containerName, 'WindowsAzure.gif');
+            
+            $result = $storageClient->listBlobs($containerName);
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Create large file
+     * 
+     * @return string Filename
+     */
+    private function _createLargeFile()
+    {
+        $fileName = tempnam('', 'tst');
+        $fp = fopen($fileName, 'w');
+        for ($i = 0; $i < Zend_Service_WindowsAzure_Storage_Blob::MAX_BLOB_SIZE / 1024; $i++)
+        {
+            fwrite($fp,
+            	'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' .
+                'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+            );
+        }
+        fclose($fp);
+        return $fileName;
+    }
+}
+
+// Call Zend_Service_WindowsAzure_BlobStorageTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_BlobStorageTest::main") {
+    Zend_Service_WindowsAzure_BlobStorageTest::main();
+}

+ 314 - 0
tests/Zend/Service/WindowsAzure/BlobStreamTest.php

@@ -0,0 +1,314 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStreamTest.php 24354 2009-07-24 08:48:54Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_BlobStreamTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_Blob */
+require_once 'Zend/Service/WindowsAzure/Storage/Blob.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStreamTest.php 24354 2009-07-24 08:48:54Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_BlobStreamTest extends PHPUnit_Framework_TestCase
+{
+    static $path;
+    
+    public function __construct()
+    {
+        self::$path = dirname(__FILE__).'/_files/';
+    }
+    
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_BlobStreamTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+   
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteContainer(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSTREAM_CONTAINER_PREFIX . $i); } catch (Exception $e) { }
+            try { $storageClient->unregisterStreamWrapper('azure'); } catch (Exception $e) { }
+        }
+    }
+
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Blob(TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOBSTREAM_CONTAINER_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test read file
+     */
+    public function testReadFile()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $fileName = 'azure://' . $containerName . '/test.txt';
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            $fh = fopen($fileName, 'w');
+            fwrite($fh, "Hello world!");
+            fclose($fh);
+            
+            $result = file_get_contents($fileName);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $this->assertEquals('Hello world!', $result);
+        }
+    }
+    
+    /**
+     * Test write file
+     */
+    public function testWriteFile()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $fileName = 'azure://' . $containerName . '/test.txt';
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            $fh = fopen($fileName, 'w');
+            fwrite($fh, "Hello world!");
+            fclose($fh);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $instance = $storageClient->getBlobInstance($containerName, 'test.txt');
+            $this->assertEquals('test.txt', $instance->Name);
+        }
+    }
+    
+    /**
+     * Test unlink file
+     */
+    public function testUnlinkFile()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $fileName = 'azure://' . $containerName . '/test.txt';
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            $fh = fopen($fileName, 'w');
+            fwrite($fh, "Hello world!");
+            fclose($fh);
+            
+            unlink($fileName);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $result = $storageClient->listBlobs($containerName);
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Test copy file
+     */
+    public function testCopyFile()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $sourceFileName = 'azure://' . $containerName . '/test.txt';
+            $destinationFileName = 'azure://' . $containerName . '/test2.txt';
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            $fh = fopen($sourceFileName, 'w');
+            fwrite($fh, "Hello world!");
+            fclose($fh);
+
+            copy($sourceFileName, $destinationFileName);
+
+            $storageClient->unregisterStreamWrapper();
+            
+            $instance = $storageClient->getBlobInstance($containerName, 'test2.txt');
+            $this->assertEquals('test2.txt', $instance->Name);
+        }
+    }
+    
+    /**
+     * Test rename file
+     */
+    public function testRenameFile()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $sourceFileName = 'azure://' . $containerName . '/test.txt';
+            $destinationFileName = 'azure://' . $containerName . '/test2.txt';
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            $fh = fopen($sourceFileName, 'w');
+            fwrite($fh, "Hello world!");
+            fclose($fh);
+            
+            rename($sourceFileName, $destinationFileName);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $instance = $storageClient->getBlobInstance($containerName, 'test2.txt');
+            $this->assertEquals('test2.txt', $instance->Name);
+        }
+    }
+    
+    /**
+     * Test mkdir
+     */
+    public function testMkdir()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            mkdir('azure://' . $containerName);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $result = $storageClient->listContainers();
+            
+            $this->assertEquals(1, count($result));
+            $this->assertEquals($containerName, $result[0]->Name);
+        }
+    }
+    
+    /**
+     * Test rmdir
+     */
+    public function testRmdir()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            
+            $storageClient = $this->createStorageInstance();
+            $storageClient->registerStreamWrapper();
+            
+            mkdir('azure://' . $containerName);
+            rmdir('azure://' . $containerName);
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $result = $storageClient->listContainers();
+            
+            $this->assertEquals(0, count($result));
+        }
+    } 
+    
+    /**
+     * Test opendir
+     */
+    public function testOpendir()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_BLOB_RUNTESTS) {
+            $containerName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createContainer($containerName);
+            
+            $storageClient->putBlob($containerName, 'images/WindowsAzure1.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure2.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure3.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure4.gif', self::$path . 'WindowsAzure.gif');
+            $storageClient->putBlob($containerName, 'images/WindowsAzure5.gif', self::$path . 'WindowsAzure.gif');
+            
+            $result1 = $storageClient->listBlobs($containerName);
+  
+            $storageClient->registerStreamWrapper();
+            
+            $result2 = array();
+            if ($handle = opendir('azure://' . $containerName)) {
+                while (false !== ($file = readdir($handle))) {
+                    $result2[] = $file;
+                }
+                closedir($handle);
+            }
+            
+            $storageClient->unregisterStreamWrapper();
+            
+            $result = $storageClient->listContainers();
+            
+            $this->assertEquals(count($result1), count($result2));
+        }
+    } 
+}
+
+// Call Zend_Service_WindowsAzure_BlobStreamTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_BlobStreamTest::main") {
+    Zend_Service_WindowsAzure_BlobStreamTest::main();
+}

+ 64 - 0
tests/Zend/Service/WindowsAzure/Credentials/AllTests.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_Credentials_AllTests::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../../TestHelper.php';
+
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyTest.php';
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyLiteTest.php';
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedAccessSignatureTest.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Credentials_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Service_WindowsAzure');
+
+        $suite->addTestSuite('Zend_Service_WindowsAzure_Credentials_SharedKeyTest');
+        $suite->addTestSuite('Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest');
+        $suite->addTestSuite('Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest');
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Service_WindowsAzure_Credentials_AllTests::main') {
+    Zend_Service_WindowsAzure_Credentials_AllTests::main();
+}

+ 143 - 0
tests/Zend/Service/WindowsAzure/Credentials/SharedAccessSignatureTest.php

@@ -0,0 +1,143 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Credentials_SharedAccessSignature */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedAccessSignature.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+    
+    /**
+     * Test signing a container
+     */
+    public function testGenerateSignatureContainer()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature('myaccount', 'WXuEUKMijV/pxUu5/RhDn1bYRuFlLSbmLUJJWRqYQ/uxbMpEx+7S/jo9sT3ZIkEucZGbEafDuxD1kwFOXf3xyw==', false);
+        $result = $credentials->createSignature(
+            'pictures',
+            'c',
+            'r',
+            '2009-02-09',
+            '2009-02-10',
+            'YWJjZGVmZw=='
+        );
+        $this->assertEquals('TEfqYYiY9Qrb7fH7nhiRCP9o5BzfO/VL8oYgfVpUl6s=', $result);
+    }
+    
+    /**
+     * Test signing a blob
+     */
+    public function testGenerateSignatureBlob()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature('myaccount', 'WXuEUKMijV/pxUu5/RhDn1bYRuFlLSbmLUJJWRqYQ/uxbMpEx+7S/jo9sT3ZIkEucZGbEafDuxD1kwFOXf3xyw==', false);
+        $result = $credentials->createSignature(
+            'pictures/blob.txt',
+            'b',
+            'r',
+            '2009-08-14T11:03:40Z',
+            '2009-08-14T11:53:40Z'
+        );
+        $this->assertEquals('hk78uZGGWd8B2NYoBwKSPs5gen3xYqsd3DPO8BQhgTU=', $result);
+    }
+    
+    /**
+     * Test container signed query string
+     */
+    public function testContainerSignedQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature('myaccount', '', false);
+        $result = $credentials->createSignedQueryString(
+            'pictures',
+            '',
+            'c',
+            'r',
+            '2009-02-09',
+            '2009-02-10',
+            'YWJjZGVmZw=='
+        );
+        $this->assertEquals('st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3D%3D&sig=iLe%2BC%2Be85l8%2BMneC9psdTCg7hJxKh314aRq3SnqPuyM%3D', $result);
+    }
+    
+    /**
+     * Test blob signed query string
+     */
+    public function testBlobSignedQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature('myaccount', '', false);
+        $result = $credentials->createSignedQueryString(
+            'pictures/blob.txt',
+        	'',
+            'b',
+            'w',
+            '2009-02-09',
+            '2009-02-10'
+        );
+        $this->assertEquals('st=2009-02-09&se=2009-02-10&sr=b&sp=w&sig=MUrHltHOJkj4425gorWWKr%2FO6mHC3XeRQ2MD6jn8jI8%3D', $result);
+    }
+    
+    /**
+     * Test sign request URL
+     */
+    public function testSignRequestUrl()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedAccessSignature('myaccount', '', false);
+        $queryString = $credentials->createSignedQueryString('pictures/blob.txt', '', 'b', 'r', '2009-02-09', '2009-02-10');
+        
+        $credentials->setPermissionSet(array(
+            'http://blob.core.windows.net/myaccount/pictures/blob.txt?' . $queryString
+        ));
+
+        $requestUrl = 'http://blob.core.windows.net/myaccount/pictures/blob.txt?comp=metadata';
+        $result = $credentials->signRequestUrl($requestUrl, Zend_Service_WindowsAzure_Storage::RESOURCE_BLOB);
+        
+        $this->assertEquals('http://blob.core.windows.net/myaccount/pictures/blob.txt?comp=metadata&' . $queryString, $result);
+    }
+}
+
+// Call Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest::main") {
+    Zend_Service_WindowsAzure_Credentials_SharedAccessSignatureTest::main();
+}

+ 174 - 0
tests/Zend/Service/WindowsAzure/Credentials/SharedKeyLiteTest.php

@@ -0,0 +1,174 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: Credentials_SharedKeyTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Credentials_SharedKeyLite */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php';
+
+/** Zend_Service_WindowsAzure_Credentials_SharedKey */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKey.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: Credentials_SharedKeyTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Test signing for devstore with root path
+     */
+    public function testSignForDevstoreWithRootPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+                          
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite devstoreaccount1:iRQpXGzlMRb1A57bkcryX7Bg/3Uf5YOfNCG+XIingJI=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for devstore with other path
+     */
+    public function testSignForDevstoreWithOtherPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/test',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite devstoreaccount1:MsC5SIbFB4M4UZd83CiMaL8ibUhaS5H9CcJBJpsnWqo=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for devstore with query string
+     */
+    public function testSignForDevstoreWithQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '?test=true',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite devstoreaccount1:iRQpXGzlMRb1A57bkcryX7Bg/3Uf5YOfNCG+XIingJI=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with root path
+     */
+    public function testSignForProductionWithRootPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+                          
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite testing:vZdOn/j0gW5FG0kAUG9NhSBO9eBjZqfe6RwALPYUtqU=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with other path
+     */
+    public function testSignForProductionWithOtherPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/test',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite testing:HJTSiRDtMsQVsFVispSHkcODeFykLO+WEuOepwmh51o=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with query string
+     */
+    public function testSignForProductionWithQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '?test=true',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKeyLite testing:vZdOn/j0gW5FG0kAUG9NhSBO9eBjZqfe6RwALPYUtqU=", $signedHeaders["Authorization"]);
+    }
+}
+
+// Call Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest::main") {
+    Zend_Service_WindowsAzure_Credentials_SharedKeyLiteTest::main();
+}

+ 171 - 0
tests/Zend/Service/WindowsAzure/Credentials/SharedKeyTest.php

@@ -0,0 +1,171 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_Credentials_SharedKeyTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Credentials_SharedKey */
+require_once 'Zend/Service/WindowsAzure/Credentials/SharedKey.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_Credentials_SharedKeyTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Test signing for devstore with root path
+     */
+    public function testSignForDevstoreWithRootPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+                          
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey devstoreaccount1:ijwRTxfJgqvmfdWPSLCpgxfvHpl6Kbbo/qJTzlI7wUw=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for devstore with other path
+     */
+    public function testSignForDevstoreWithOtherPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/test',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey devstoreaccount1:ZLs/nBsEaoyCqHpqcQUfXO5zIHBTMcrzVaIxwQNBL9k=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for devstore with query string
+     */
+    public function testSignForDevstoreWithQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey(Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_ACCOUNT, Zend_Service_WindowsAzure_Credentials_SharedKey::DEVSTORE_KEY, true);
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '?test=true',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey devstoreaccount1:ijwRTxfJgqvmfdWPSLCpgxfvHpl6Kbbo/qJTzlI7wUw=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with root path
+     */
+    public function testSignForProductionWithRootPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+                          
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey testing:TEYBENKs+6laykL+zCxlIbUT9v019rtMWECYwgP/OuU=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with other path
+     */
+    public function testSignForProductionWithOtherPath()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/test',
+                              '',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey testing:d2kcDGCQ603wPuZ3KHbeILtNhIXMXyTNVn2x9d5aF60=", $signedHeaders["Authorization"]);
+    }
+    
+    /**
+     * Test signing for production with query string
+     */
+    public function testSignForProductionWithQueryString()
+    {
+        $credentials = new Zend_Service_WindowsAzure_Credentials_SharedKey('testing', 'abcdefg');
+        $signedHeaders = $credentials->signRequestHeaders(
+                              'GET',
+                              '/',
+                              '?test=true',
+                              array("x-ms-date" => "Wed, 29 Apr 2009 13:12:47 GMT"),
+                              false
+                          );
+  
+        $this->assertType('array', $signedHeaders);
+        $this->assertEquals(2, count($signedHeaders));
+        $this->assertEquals("SharedKey testing:TEYBENKs+6laykL+zCxlIbUT9v019rtMWECYwgP/OuU=", $signedHeaders["Authorization"]);
+    }
+}
+
+// Call Zend_Service_WindowsAzure_Credentials_SharedKeyTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_Credentials_SharedKeyTest::main") {
+    Zend_Service_WindowsAzure_Credentials_SharedKeyTest::main();
+}

+ 114 - 0
tests/Zend/Service/WindowsAzure/DynamicTableEntityTest.php

@@ -0,0 +1,114 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_DynamicTableEntityTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_DynamicTableEntity */
+require_once 'Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_DynamicTableEntityTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_DynamicTableEntityTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+    
+    /**
+     * Test constructor
+     */
+    public function testConstructor()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity('partition1', '000001');
+        $this->assertEquals('partition1', $target->getPartitionKey());
+        $this->assertEquals('000001',     $target->getRowKey());
+    }
+    
+    /**
+     * Test get Azure values
+     */
+    public function testGetAzureValues()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity('partition1', '000001');
+        $target->Name = 'Name';
+        $target->Age  = 25;
+        $result = $target->getAzureValues();
+
+        $this->assertEquals('Name',       $result[0]->Name);
+        $this->assertEquals('Name',       $result[0]->Value);
+        $this->assertEquals('Edm.String', $result[0]->Type);
+        
+        $this->assertEquals('Age',        $result[1]->Name);
+        $this->assertEquals(25,           $result[1]->Value);
+        $this->assertEquals('Edm.Int32',  $result[1]->Type);
+        
+        $this->assertEquals('partition1', $result[2]->Value);
+        $this->assertEquals('000001',     $result[3]->Value);
+    }
+    
+    /**
+     * Test set Azure values
+     */
+    public function testSetAzureValues()
+    {
+        $values = array(
+            'PartitionKey' => 'partition1',
+            'RowKey' => '000001',
+            'Name' => 'Maarten',
+            'Age' => 25,
+            'Visible' => true
+        );
+        
+        $target = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity();
+        $target->setAzureValues($values);
+        $target->setAzurePropertyType('Age', 'Edm.Int32');
+
+        $this->assertEquals('partition1', $target->getPartitionKey());
+        $this->assertEquals('000001',     $target->getRowKey());
+        $this->assertEquals('Maarten',    $target->Name);
+        $this->assertEquals(25,           $target->Age);
+        $this->assertEquals('Edm.Int32',  $target->getAzurePropertyType('Age'));
+        $this->assertEquals(true,         $target->Visible);
+    }
+}
+
+// Call Zend_Service_WindowsAzure_DynamicTableEntityTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_DynamicTableEntityTest::main") {
+    Zend_Service_WindowsAzure_DynamicTableEntityTest::main();
+}

+ 326 - 0
tests/Zend/Service/WindowsAzure/QueueStorageTest.php

@@ -0,0 +1,326 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: QueueStorageTest.php 24241 2009-07-22 09:43:13Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_QueueStorageTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_Queue */
+require_once 'Zend/Service/WindowsAzure/Storage/Queue.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: QueueStorageTest.php 24241 2009-07-22 09:43:13Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_QueueStorageTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS)  {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_QueueStorageTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+   
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteQueue(TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_PREFIX . $i); } catch (Exception $e) { }
+        }
+    }
+
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Queue(TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Queue(TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test queue exists
+     */
+    public function testQueueExists()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName1 = $this->generateName();
+            $queueName2 = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName1);
+            $storageClient->createQueue($queueName2);
+            
+            $result = $storageClient->queueExists($queueName1);
+            $this->assertTrue($result);
+            
+            $result = $storageClient->queueExists(md5(time()));
+            $this->assertFalse($result);
+        }
+    }
+    
+    /**
+     * Test create queue
+     */
+    public function testCreateQueue()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $result = $storageClient->createQueue($queueName);
+            $this->assertEquals($queueName, $result->Name);
+        }
+    }
+    
+    /**
+     * Test set queue metadata
+     */
+    public function testSetQueueMetadata()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            
+            $storageClient->setQueueMetadata($queueName, array(
+                'createdby' => 'PHPAzure',
+            ));
+            
+            $metadata = $storageClient->getQueueMetadata($queueName);
+            $this->assertEquals('PHPAzure', $metadata['createdby']);
+        }
+    }
+    
+    /**
+     * Test get queue
+     */
+    public function testGetQueue()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            
+            $queue = $storageClient->getQueue($queueName);
+            $this->assertEquals($queueName, $queue->Name);
+            $this->assertEquals(0, $queue->ApproximateMessageCount);
+        }
+    }
+    
+    /**
+     * Test list queues
+     */
+    public function testListQueues()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName1 = 'testlist1';
+            $queueName2 = 'testlist2';
+            $queueName3 = 'testlist3';
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName1);
+            $storageClient->createQueue($queueName2);
+            $storageClient->createQueue($queueName3);
+            $result1 = $storageClient->listQueues('testlist');
+            $result2 = $storageClient->listQueues('testlist', 1);
+    
+            // cleanup first
+            $storageClient->deleteQueue($queueName1);
+            $storageClient->deleteQueue($queueName2);
+            $storageClient->deleteQueue($queueName3);
+            
+            $this->assertEquals(3, count($result1));
+            $this->assertEquals($queueName2, $result1[1]->Name);
+            
+            $this->assertEquals(1, count($result2));
+        }
+    }
+    
+    /**
+     * Test put message
+     */
+    public function testPutMessage()
+    {
+    	if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            $storageClient->putMessage($queueName, 'Test message', 120);
+            
+            sleep(45); // wait for the message to appear in the queue...
+            
+            $messages = $storageClient->getMessages($queueName);
+            $this->assertEquals(1, count($messages));
+            $this->assertEquals('Test message', $messages[0]->MessageText);
+        }
+    }
+    
+    /**
+     * Test get messages
+     */
+    public function testGetMessages()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            $storageClient->putMessage($queueName, 'Test message 1', 120);
+            $storageClient->putMessage($queueName, 'Test message 2', 120);
+            $storageClient->putMessage($queueName, 'Test message 3', 120);
+            $storageClient->putMessage($queueName, 'Test message 4', 120);
+            
+            sleep(45); // wait for the messages to appear in the queue...
+            
+            $messages1 = $storageClient->getMessages($queueName, 2);
+            $messages2 = $storageClient->getMessages($queueName, 2);
+            $messages3 = $storageClient->getMessages($queueName);
+            
+            $this->assertEquals(2, count($messages1));
+            $this->assertEquals(2, count($messages2));
+            $this->assertEquals(0, count($messages3));
+        }
+    }
+    
+    /**
+     * Test peek messages
+     */
+    public function testPeekMessages()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            $storageClient->putMessage($queueName, 'Test message 1', 120);
+            $storageClient->putMessage($queueName, 'Test message 2', 120);
+            $storageClient->putMessage($queueName, 'Test message 3', 120);
+            $storageClient->putMessage($queueName, 'Test message 4', 120);
+            
+            sleep(45); // wait for the messages to appear in the queue...
+            
+            $messages1 = $storageClient->peekMessages($queueName, 4);
+            $messages2 = $storageClient->getMessages($queueName, 4);
+            
+            $this->assertEquals(4, count($messages1));
+            $this->assertEquals(4, count($messages2));
+        }
+    }
+    
+    /**
+     * Test clear messages
+     */
+    public function testClearMessages()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            $storageClient->putMessage($queueName, 'Test message 1', 120);
+            $storageClient->putMessage($queueName, 'Test message 2', 120);
+            $storageClient->putMessage($queueName, 'Test message 3', 120);
+            $storageClient->putMessage($queueName, 'Test message 4', 120);
+            
+            sleep(45); // wait for the messages to appear in the queue...
+            
+            $messages1 = $storageClient->peekMessages($queueName, 4);
+            $storageClient->clearMessages($queueName);
+            
+            sleep(45); // wait for the GC...
+            
+            $messages2 = $storageClient->peekMessages($queueName, 4);
+            
+            $this->assertEquals(4, count($messages1));
+            $this->assertEquals(0, count($messages2));
+        }
+    }
+    
+    /**
+     * Test delete message
+     */
+    public function testDeleteMessage()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_QUEUE_RUNTESTS) {
+            $queueName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createQueue($queueName);
+            $storageClient->putMessage($queueName, 'Test message 1', 120);
+            $storageClient->putMessage($queueName, 'Test message 2', 120);
+            $storageClient->putMessage($queueName, 'Test message 3', 120);
+            $storageClient->putMessage($queueName, 'Test message 4', 120);
+            
+            sleep(45); // wait for the messages to appear in the queue...
+            
+            $messages1 = $storageClient->getMessages($queueName, 2, 10);
+            foreach ($messages1 as $message)
+            {
+                $storageClient->deleteMessage($queueName, $message);
+            }
+            
+            sleep(45); // wait for the GC...
+            
+            $messages2 = $storageClient->getMessages($queueName, 4);
+            
+            $this->assertEquals(2, count($messages1));
+            $this->assertEquals(2, count($messages2));
+        }
+    }
+}
+
+// Call Zend_Service_WindowsAzure_QueueStorageTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_QueueStorageTest::main") {
+    Zend_Service_WindowsAzure_QueueStorageTest::main();
+}

+ 119 - 0
tests/Zend/Service/WindowsAzure/RetryPolicyTest.php

@@ -0,0 +1,119 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: RetryPolicyTest.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_RetryPolicyTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract */
+require_once 'Zend/Service/WindowsAzure/RetryPolicy/RetryPolicyAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: RetryPolicyTest.php 35709 2009-12-14 14:14:14Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_RetryPolicyTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Helper variable for counting retries
+     * 
+     * @var int
+     */
+    protected $_executedRetries = 0;
+    
+    /**
+     * Helper variable for setting Exception count 
+     * 
+     * @var int
+     */
+    protected $_exceptionCount = 0;
+    
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_RetryPolicyTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Test retry policy - noRetry
+     */
+    public function testNoRetry()
+    {
+        $this->_executedRetries = 0;
+        $policy = Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::noRetry();
+        $retries = $policy->execute(
+            array($this, '_countRetries')
+        );
+        $this->assertEquals(1, $retries);
+    }
+    
+    /**
+     * Test retry policy - retryN
+     */
+    public function testRetryN()
+    {
+        $this->_executedRetries = 0;
+        $this->_exceptionCount = 9;
+        
+        $policy = Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 100);
+        $retries = $policy->execute(
+            array($this, '_countRetriesAndThrowExceptions')
+        );
+        $this->assertEquals(10, $retries);
+    }
+    
+    /**
+     * Helper function, counting retries
+     */
+    public function _countRetries()
+    {
+        return ++$this->_executedRetries;
+    }
+    
+    /**
+     * Helper function, counting retries and generating number of exceptions
+     */
+    public function _countRetriesAndThrowExceptions()
+    {
+        ++$this->_executedRetries;
+        if ($this->_exceptionCount-- > 0) {
+            throw new Exception("Exception thrown.");
+        }
+        return $this->_executedRetries;
+    }
+}
+
+// Call Zend_Service_WindowsAzure_RetryPolicyTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_RetryPolicyTest::main") {
+    Zend_Service_WindowsAzure_RetryPolicyTest::main();
+}

+ 263 - 0
tests/Zend/Service/WindowsAzure/SessionHandlerTest.php

@@ -0,0 +1,263 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_SessionHandlerTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_SessionHandler */
+require_once 'Zend/Service/WindowsAzure/SessionHandler.php';
+
+/** Zend_Service_WindowsAzure_Storage_Table */
+require_once 'Zend/Service/WindowsAzure/Storage/Table.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_SessionHandlerTest extends PHPUnit_Framework_TestCase
+{
+    public function __construct()
+    {
+    }
+    
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_SessionHandlerTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+    
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteTable(TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_TABLENAME_PREFIX . $i); } catch (Exception $e) { }
+        }
+    }
+    
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Table(TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Table(TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected function createSessionHandler($storageInstance, $tableName)
+    {
+        $sessionHandler = new Zend_Service_WindowsAzure_SessionHandler(
+            $storageInstance,
+            $tableName
+        );
+        return $sessionHandler;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_TABLENAME_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test register
+     */
+    public function testRegister()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $result = $sessionHandler->register();
+            
+            $this->assertTrue($result);
+        }
+    }
+    
+    /**
+     * Test open
+     */
+    public function testOpen()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $result = $sessionHandler->open();
+
+            $this->assertTrue($result);
+            
+            
+            $verifyResult = $storageClient->listTables();
+            $this->assertEquals($tableName, $verifyResult[0]->Name);
+        }
+    }
+    
+    /**
+     * Test close
+     */
+    public function testClose()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $sessionHandler->open();
+            $result = $sessionHandler->close();
+            
+            $this->assertTrue($result);
+        }
+    }
+    
+    /**
+     * Test read
+     */
+    public function testRead()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $sessionHandler->open();
+            
+            $sessionId = $this->session_id();
+            $sessionData = serialize( 'PHPAzure' );
+            $sessionHandler->write($sessionId, $sessionData);
+            
+            $result = unserialize( $sessionHandler->read($sessionId) );
+            
+            $this->assertEquals('PHPAzure', $result);
+        }
+    }
+    
+    /**
+     * Test write
+     */
+    public function testWrite()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $sessionHandler->open();
+            
+            $sessionId = $this->session_id();
+            $sessionData = serialize( 'PHPAzure' );
+            $sessionHandler->write($sessionId, $sessionData);
+            
+            
+            $verifyResult = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(1, count($verifyResult));
+        }
+    }
+    
+    /**
+     * Test destroy
+     */
+    public function testDestroy()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $sessionHandler->open();
+            
+            $sessionId = $this->session_id();
+            $sessionData = serialize( 'PHPAzure' );
+            $sessionHandler->write($sessionId, $sessionData);
+            
+            $result = $sessionHandler->destroy($sessionId);
+            $this->assertTrue($result);
+            
+            $verifyResult = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(0, count($verifyResult));
+        }
+    }
+    
+    /**
+     * Test gc
+     */
+    public function testGc()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_SESSIONHANDLER_RUNTESTS) {
+            $storageClient = $this->createStorageInstance();
+            $tableName = $this->generateName();
+            $sessionHandler = $this->createSessionHandler($storageClient, $tableName);
+            $sessionHandler->open();
+            
+            $sessionId = $this->session_id();
+            $sessionData = serialize( 'PHPAzure' );
+            $sessionHandler->write($sessionId, $sessionData);
+            
+            sleep(1); // let time() tick
+            
+            $result = $sessionHandler->gc(0);
+            $this->assertTrue($result);
+            
+            $verifyResult = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(0, count($verifyResult));
+        }
+    }
+
+    protected function session_id()
+    {
+        return md5(self::$uniqId);
+    }
+}
+
+// Call Zend_Service_WindowsAzure_SessionHandlerTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_SessionHandlerTest::main") {
+    Zend_Service_WindowsAzure_SessionHandlerTest::main();
+}

+ 75 - 0
tests/Zend/Service/WindowsAzure/StorageTest.php

@@ -0,0 +1,75 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: StorageTest.php 28585 2009-09-07 12:12:56Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_StorageTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage */
+require_once 'Zend/Service/WindowsAzure/Storage.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: StorageTest.php 28585 2009-09-07 12:12:56Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_StorageTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_BlobStorageTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Test constructor for devstore
+     */
+    public function testConstructorForDevstore()
+    {
+        $storage = new Zend_Service_WindowsAzure_Storage();
+        $this->assertEquals('http://127.0.0.1:10000/devstoreaccount1', $storage->getBaseUrl());
+    }
+    
+    /**
+     * Test constructor for production
+     */
+    public function testConstructorForProduction()
+    {
+        $storage = new Zend_Service_WindowsAzure_Storage(Zend_Service_WindowsAzure_Storage::URL_CLOUD_BLOB, 'testing', '');
+        $this->assertEquals('http://testing.blob.core.windows.net', $storage->getBaseUrl());
+    }
+}
+
+// Call Zend_Service_WindowsAzure_StorageTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_StorageTest::main") {
+    Zend_Service_WindowsAzure_StorageTest::main();
+}

+ 189 - 0
tests/Zend/Service/WindowsAzure/TableEntityQueryTest.php

@@ -0,0 +1,189 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_TableEntityQueryTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_TableEntityQuery */
+require_once 'Zend/Service/WindowsAzure/Storage/TableEntityQuery.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_TableEntityQueryTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_TableEntityQueryTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+    
+    /**
+     * Test all records query
+     */
+    public function testAllRecordsQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable');
+               
+        $this->assertEquals('MyTable()', $target->__toString());
+    }
+    
+    /**
+     * Test partition key query
+     */
+    public function testPartitionKeyQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->wherePartitionKey('test');
+               
+        $this->assertEquals('MyTable(PartitionKey=\'test\')', $target->__toString());
+    }
+    
+    /**
+     * Test row key query
+     */
+    public function testRowKeyQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->whereRowKey('test');
+               
+        $this->assertEquals('MyTable(RowKey=\'test\')', $target->__toString());
+    }
+    
+    /**
+     * Test identifier query
+     */
+    public function testIdentifierQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->wherePartitionKey('test')
+               ->whereRowKey('123');
+               
+        $this->assertEquals('MyTable(PartitionKey=\'test\', RowKey=\'123\')', $target->__toString());
+    }
+    
+    /**
+     * Test top records query
+     */
+    public function testTopQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->top(10);
+               
+        $this->assertEquals('MyTable()?$top=10', $target->__toString());
+    }
+    
+    /**
+     * Test order by query
+     */
+    public function testOrderByQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->orderBy('Name', 'asc');
+               
+        $this->assertEquals('MyTable()?$orderby=Name asc', $target->__toString());
+    }
+    
+    /**
+     * Test order by multiple query
+     */
+    public function testOrderByMultipleQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->orderBy('Name', 'asc')
+               ->orderBy('Visible', 'desc');
+               
+        $this->assertEquals('MyTable()?$orderby=Name asc,Visible desc', $target->__toString());
+    }
+    
+    /**
+     * Test where query
+     */
+    public function testWhereQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->where('Name eq ?', 'Maarten');
+               
+        $this->assertEquals('MyTable()?$filter=Name eq \'Maarten\'', $target->__toString());
+    }
+    
+    /**
+     * Test where array query
+     */
+    public function testWhereArrayQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->where('Name eq ? or Name eq ?', array('Maarten', 'Vijay'));
+               
+        $this->assertEquals('MyTable()?$filter=Name eq \'Maarten\' or Name eq \'Vijay\'', $target->__toString());
+    }
+    
+    /**
+     * Test where multiple query
+     */
+    public function testWhereMultipleQuery()
+    {
+        $target = new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
+        $target->select()
+               ->from('MyTable')
+               ->where('Name eq ?', 'Maarten')
+               ->andWhere('Visible eq true');
+               
+        $this->assertEquals('MyTable()?$filter=Name eq \'Maarten\' and Visible eq true', $target->__toString());
+    }
+}
+
+// Call Zend_Service_WindowsAzure_TableEntityQueryTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_TableEntityQueryTest::main") {
+    Zend_Service_WindowsAzure_TableEntityQueryTest::main();
+}

+ 155 - 0
tests/Zend/Service/WindowsAzure/TableEntityTest.php

@@ -0,0 +1,155 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_TableEntityTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_TableEntity */
+require_once 'Zend/Service/WindowsAzure/Storage/TableEntity.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_TableEntityTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_TableEntityTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+    
+    /**
+     * Test constructor
+     */
+    public function testConstructor()
+    {
+        $target = new TSETTest_TestEntity('partition1', '000001');
+        $this->assertEquals('partition1', $target->getPartitionKey());
+        $this->assertEquals('000001',     $target->getRowKey());
+    }
+    
+    /**
+     * Test get Azure values
+     */
+    public function testGetAzureValues()
+    {
+        $target = new TSETTest_TestEntity('partition1', '000001');
+        $result = $target->getAzureValues();
+        
+        $this->assertEquals('Name',       $result[0]->Name);
+        $this->assertEquals(null,         $result[0]->Value);
+        
+        $this->assertEquals('Age',        $result[1]->Name);
+        $this->assertEquals('Edm.Int64',  $result[1]->Type);
+        
+        $this->assertEquals('Visible',    $result[2]->Name);
+        $this->assertEquals(false,        $result[2]->Value);
+        
+        $this->assertEquals('partition1', $result[3]->Value);
+        $this->assertEquals('000001',     $result[4]->Value);
+    }
+    
+    /**
+     * Test set Azure values
+     */
+    public function testSetAzureValuesSuccess()
+    {
+        $values = array(
+            'PartitionKey' => 'partition1',
+            'RowKey' => '000001',
+            'Name' => 'Maarten',
+            'Age' => 25,
+            'Visible' => true
+        );
+        
+        $target = new TSETTest_TestEntity();
+        $target->setAzureValues($values);
+        
+        $this->assertEquals('partition1', $target->getPartitionKey());
+        $this->assertEquals('000001',     $target->getRowKey());
+        $this->assertEquals('Maarten',    $target->FullName);
+        $this->assertEquals(25,           $target->Age);
+        $this->assertEquals(true,         $target->Visible);
+    }
+    
+    /**
+     * Test set Azure values
+     */
+    public function testSetAzureValuesFailure()
+    {
+        $values = array(
+            'PartitionKey' => 'partition1',
+            'RowKey' => '000001'
+        );
+        
+        $exceptionRaised = false;
+        $target = new TSETTest_TestEntity();
+        try 
+        {
+            $target->setAzureValues($values, true);
+        }
+        catch (Exception $ex) {
+            $exceptionRaised = true;
+        }
+        
+        $this->assertTrue($exceptionRaised);
+    }
+}
+
+/**
+ * Test entity
+ */
+class TSETTest_TestEntity extends Zend_Service_WindowsAzure_Storage_TableEntity
+{
+    /**
+     * @azure Name
+     */
+    public $FullName;
+    
+    /**
+     * @azure Age Edm.Int64
+     */
+    public $Age;
+    
+    /**
+     * @azure Visible Edm.Boolean
+     */
+    public $Visible = false;
+}
+
+// Call Zend_Service_WindowsAzure_TableEntityTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_TableEntityTest::main") {
+    Zend_Service_WindowsAzure_TableEntityTest::main();
+}

+ 790 - 0
tests/Zend/Service/WindowsAzure/TableStorageTest.php

@@ -0,0 +1,790 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Service_WindowsAzure_TableStorageTest::main');
+}
+
+/**
+ * Test helpers
+ */
+require_once dirname(__FILE__) . '/../../../TestHelper.php';
+require_once dirname(__FILE__) . '/../../../TestConfiguration.php.dist';
+require_once 'PHPUnit/Framework/TestCase.php';
+
+/** Zend_Service_WindowsAzure_Storage_Table */
+require_once 'Zend/Service/WindowsAzure/Storage/Table.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Service_WindowsAzure
+ * @subpackage UnitTests
+ * @version    $Id: BlobStorageTest.php 14561 2009-05-07 08:05:12Z unknown $
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Service_WindowsAzure_TableStorageTest extends PHPUnit_Framework_TestCase
+{
+    public function __construct()
+    {
+    }
+    
+    public static function main()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $suite  = new PHPUnit_Framework_TestSuite("Zend_Service_WindowsAzure_TableStorageTest");
+            $result = PHPUnit_TextUI_TestRunner::run($suite);
+        }
+    }
+    
+    /**
+     * Test setup
+     */
+    protected function setUp()
+    {
+    }
+    
+    /**
+     * Test teardown
+     */
+    protected function tearDown()
+    {
+        $storageClient = $this->createStorageInstance();
+        for ($i = 1; $i <= self::$uniqId; $i++)
+        {
+            try { $storageClient->deleteTable(TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_TABLENAME_PREFIX . $i); } catch (Exception $e) { }
+        }
+    }
+    
+    protected function createStorageInstance()
+    {
+        $storageClient = null;
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNONPROD) {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Table(TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_PROD, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_PROD, false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        } else {
+            $storageClient = new Zend_Service_WindowsAzure_Storage_Table(TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_HOST_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_ACCOUNT_DEV, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_KEY_DEV, true, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract::retryN(10, 250));
+        }
+        
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY) {
+            $storageClient->setProxy(TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_USEPROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_PORT, TESTS_ZEND_SERVICE_WINDOWSAZURE_STORAGE_PROXY_CREDENTIALS);
+        }
+
+        return $storageClient;
+    }
+    
+    protected static $uniqId = 0;
+    
+    protected function generateName()
+    {
+        self::$uniqId++;
+        return TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_TABLENAME_PREFIX . self::$uniqId;
+    }
+    
+    /**
+     * Test create table
+     */
+    public function testCreateTable()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            
+            $result = $storageClient->createTable($tableName);
+            $this->assertEquals($tableName, $result->Name);
+            
+            $result = $storageClient->listTables();
+            $this->assertEquals(1, count($result));
+            $this->assertEquals($tableName, $result[0]->Name);
+        }
+    }
+    
+    /**
+     * Test table exists
+     */
+    public function testTableExists()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName1 = $this->generateName();
+            $tableName2 = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            
+            $storageClient->createTable($tableName1);
+            $storageClient->createTable($tableName2);
+
+            $result = $storageClient->tableExists($tableName2);
+            $this->assertTrue($result);
+            
+            $result = $storageClient->tableExists(md5(time()));
+            $this->assertFalse($result);
+        }
+    }
+    
+    /**
+     * Test list tables
+     */
+    public function testListTables()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName1 = $this->generateName();
+            $tableName2 = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            
+            $storageClient->createTable($tableName1);
+            $storageClient->createTable($tableName2);
+
+            $result = $storageClient->listTables();
+            $this->assertEquals(2, count($result));
+            $this->assertEquals($tableName1, $result[0]->Name);
+            $this->assertEquals($tableName2, $result[1]->Name);
+        }
+    }
+    
+    /**
+     * Test delete table
+     */
+    public function testDeleteTable()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            
+            $storageClient->createTable($tableName);
+            $storageClient->deleteTable($tableName);
+            
+            $result = $storageClient->listTables();
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Test insert entity
+     */
+    public function testInsertEntity()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $result = $storageClient->insertEntity($tableName, $entity);
+
+            $this->assertNotEquals('0001-01-01T00:00:00', $result->getTimestamp());
+            $this->assertNotEquals('', $result->getEtag());
+            $this->assertEquals($entity, $result);
+        }
+    }
+    
+    /**
+     * Test delete entity, not taking etag into account
+     */
+    public function testDeleteEntity_NoEtag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $result = $storageClient->insertEntity($tableName, $entity);
+            
+            $this->assertEquals($entity, $result);
+            
+            $storageClient->deleteEntity($tableName, $entity);
+        }
+    }
+    
+    /**
+     * Test delete entity, taking etag into account
+     */
+    public function testDeleteEntity_Etag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $result = $storageClient->insertEntity($tableName, $entity);
+
+            $this->assertEquals($entity, $result);
+
+            // Set "old" etag
+            $entity->setEtag('W/"datetime\'2009-05-27T12%3A15%3A15.3321531Z\'"');
+            
+            $exceptionThrown = false;
+            try {
+                $storageClient->deleteEntity($tableName, $entity, true);
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+    
+    /**
+     * Test retrieve entity by id
+     */
+    public function testRetrieveEntityById()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS)  {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            
+            $result = $storageClient->retrieveEntityById($tableName, $entity->getPartitionKey(), $entity->getRowKey(), 'TSTest_TestEntity');
+            $this->assertEquals($entity, $result);
+        }
+    }
+    
+    /**
+     * Test retrieve entity by id (> 256 key characters)
+     */
+    public function testRetrieveEntityById_Large()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS)  {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            $entity->setPartitionKey(str_repeat('a', 200));
+            $entity->setRowKey(str_repeat('a', 200));
+            
+            $storageClient->insertEntity($tableName, $entity);
+            
+            $result = $storageClient->retrieveEntityById($tableName, $entity->getPartitionKey(), $entity->getRowKey(), 'TSTest_TestEntity');
+            $this->assertEquals($entity, $result);
+        }
+    }
+    
+    /**
+     * Test retrieve entity by id, DynamicTableEntity
+     */
+    public function testRetrieveEntityById_DynamicTableEntity()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS)  {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            
+            $result = $storageClient->retrieveEntityById($tableName, $entity->getPartitionKey(), $entity->getRowKey());
+            $this->assertEquals($entity->FullName, $result->Name);
+            $this->assertType('Zend_Service_WindowsAzure_Storage_DynamicTableEntity', $result);
+        }
+    }
+    
+    /**
+     * Test update entity, not taking etag into account
+     */
+    public function testUpdateEntity_NoEtag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            $entity->Age = 0;
+            
+            $result = $storageClient->updateEntity($tableName, $entity);
+
+            $this->assertNotEquals('0001-01-01T00:00:00', $result->getTimestamp());
+            $this->assertNotEquals('', $result->getEtag());
+            $this->assertEquals(0, $result->Age);
+            $this->assertEquals($entity, $result);
+        }
+    }
+    
+    /**
+     * Test update entity, taking etag into account
+     */
+    public function testUpdateEntity_Etag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            $entity->Age = 0;
+            
+            // Set "old" etag
+            $entity->setEtag('W/"datetime\'2009-05-27T12%3A15%3A15.3321531Z\'"');
+            
+            $exceptionThrown = false;
+            try {
+                $storageClient->updateEntity($tableName, $entity, true);
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+    
+    /**
+     * Test merge entity, not taking etag into account
+     */
+    public function testMergeEntity_NoEtag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            
+            $dynamicEntity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
+            $dynamicEntity->Myproperty = 10;
+            $dynamicEntity->Otherproperty = "Test";
+            $dynamicEntity->Age = 0;
+            
+            $storageClient->mergeEntity($tableName, $dynamicEntity, false, array('Myproperty', 'Otherproperty')); // only update 'Myproperty' and 'Otherproperty'
+            
+            $result = $storageClient->retrieveEntityById($tableName, $entity->getPartitionKey(), $entity->getRowKey());
+
+            $this->assertNotEquals('0001-01-01T00:00:00', $result->getTimestamp());
+            $this->assertNotEquals('', $result->getEtag());
+            $this->assertNotEquals(0, $result->Age);
+            $this->assertEquals($entity->FullName, $result->Name);
+            $this->assertEquals($dynamicEntity->Myproperty, $result->Myproperty);
+            $this->assertEquals($dynamicEntity->Otherproperty, $result->Otherproperty);
+        }
+    }
+    
+    /**
+     * Test merge entity, taking etag into account
+     */
+    public function testMergeEntity_Etag()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(1);
+            $entity = $entities[0];
+            
+            $storageClient->insertEntity($tableName, $entity);
+            
+            $dynamicEntity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
+            $dynamicEntity->Myproperty = 10;
+            $dynamicEntity->Otherproperty = "Test";
+            $dynamicEntity->Age = 0;
+            
+            // Set "old" etag
+            $entity->setEtag('W/"datetime\'2009-05-27T12%3A15%3A15.3321531Z\'"');
+            
+            $exceptionThrown = false;
+            try {
+                $storageClient->mergeEntity($tableName, $dynamicEntity, true);
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+    
+    /**
+     * Test retrieve entities, all
+     */
+    public function testRetrieveEntities_All()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(20);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities($tableName, 'TSTest_TestEntity');
+            $this->assertEquals(20, count($result));
+        }
+    }
+    
+    /**
+     * Test retrieve entities, all, DynamicTableEntity
+     */
+    public function testRetrieveEntities_All_DynamicTableEntity()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(20);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(20, count($result));
+            
+            foreach ($result as $item)
+            {
+                $this->assertType('Zend_Service_WindowsAzure_Storage_DynamicTableEntity', $item);
+            }
+        }
+    }
+    
+    /**
+     * Test retrieve entities, filtered
+     */
+    public function testRetrieveEntities_Filtered()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(5);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities($tableName, 'PartitionKey eq \'' . $entities[0]->getPartitionKey() . '\' and RowKey eq \'' . $entities[0]->getRowKey() . '\'', 'TSTest_TestEntity');
+            $this->assertEquals(1, count($result));
+        }
+    }
+    
+    /**
+     * Test retrieve entities, fluent interface
+     */
+    public function testRetrieveEntities_Fluent1()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities(
+                $storageClient->select()
+                              ->from($tableName)
+                              ->where('Name eq ?', $entities[0]->FullName)
+                              ->andWhere('RowKey eq ?', $entities[0]->getRowKey()),
+                'TSTest_TestEntity'
+            );
+            
+            $this->assertEquals(1, count($result));
+            $this->assertEquals($entities[0], $result[0]);
+        }
+    }
+    
+    /**
+     * Test retrieve entities, fluent interface
+     */
+    public function testRetrieveEntities_Fluent2()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities(
+                $storageClient->select()
+                              ->from($tableName)
+                              ->where('Name eq ?', $entities[0]->FullName)
+                              ->andWhere('PartitionKey eq ?', $entities[0]->getPartitionKey()),
+                'TSTest_TestEntity'
+            );
+            
+            $this->assertEquals(1, count($result));
+            $this->assertEquals($entities[0], $result[0]);
+        }
+    }
+    
+    /**
+     * Test retrieve entities, fluent interface, top specification
+     */
+    public function testRetrieveEntities_Fluent_Top()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            $result = $storageClient->retrieveEntities(
+                $storageClient->select()->top(4)
+                              ->from($tableName),
+                'TSTest_TestEntity'
+            );
+            
+            $this->assertEquals(4, count($result));
+        }
+    }
+    
+    /**
+     * Test batch commit, success
+     */
+    public function testBatchCommit_Success()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(20);
+            $entities1 = array_slice($entities, 0, 10);
+            $entities2 = array_slice($entities, 10, 10);
+            
+            // Insert entities
+            foreach ($entities1 as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            // Start batch
+            $batch = $storageClient->startBatch();
+            $this->assertType('Zend_Service_WindowsAzure_Storage_Batch', $batch);
+            
+            // Insert entities in batch
+            foreach ($entities2 as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            // Delete entities
+            foreach ($entities1 as $entity)
+            {
+                $storageClient->deleteEntity($tableName, $entity);
+            }
+            
+            // Commit
+            $batch->commit();
+            
+            // Verify
+            $result = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(10, count($result));
+        }
+    }
+    
+    /**
+     * Test batch rollback, success
+     */
+    public function testBatchRollback_Success()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            
+            // Start batch
+            $batch = $storageClient->startBatch();
+            $this->assertType('Zend_Service_WindowsAzure_Storage_Batch', $batch);
+            
+            // Insert entities in batch
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            // Rollback
+            $batch->rollback();
+            
+            // Verify
+            $result = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Test batch commit, fail updates
+     */
+    public function testBatchCommit_FailUpdates()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            foreach ($entities as $entity)
+            {
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            // Make some entity updates with "old" etags
+            $entities[0]->Age = 0;
+            $entities[0]->setEtag('W/"datetime\'2009-05-27T12%3A15%3A15.3321531Z\'"');
+            $entities[1]->Age = 0;
+            $entities[1]->setEtag('W/"datetime\'2009-05-27T12%3A15%3A15.3321531Z\'"');
+            $entities[2]->Age = 0;
+            
+            // Start batch
+            $batch = $storageClient->startBatch();
+            $this->assertType('Zend_Service_WindowsAzure_Storage_Batch', $batch);
+            
+            // Update entities in batch
+            $storageClient->updateEntity($tableName, $entities[0], true);
+            $storageClient->updateEntity($tableName, $entities[1], true);
+            $storageClient->updateEntity($tableName, $entities[2], true);
+            
+            // Commit
+            $exceptionThrown = false;
+            try {
+                $batch->commit();
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+        }
+    }
+    
+    /**
+     * Test batch commit, fail partition
+     */
+    public function testBatchCommit_FailPartition()
+    {
+        if (TESTS_ZEND_SERVICE_WINDOWSAZURE_TABLE_RUNTESTS) {
+            $tableName = $this->generateName();
+            $storageClient = $this->createStorageInstance();
+            $storageClient->createTable($tableName);
+            
+            $entities = $this->_generateEntities(10);
+            
+            // Start batch
+            $batch = $storageClient->startBatch();
+            $this->assertType('Zend_Service_WindowsAzure_Storage_Batch', $batch);
+            
+            // Insert entities in batch
+            foreach ($entities as $entity)
+            {
+                $entity->setPartitionKey('partition' . rand(1, 9));
+                $storageClient->insertEntity($tableName, $entity);
+            }
+            
+            // Commit
+            $exceptionThrown = false;
+            try {
+                $batch->commit();
+            } catch (Exception $ex) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+            
+            // Verify
+            $result = $storageClient->retrieveEntities($tableName);
+            $this->assertEquals(0, count($result));
+        }
+    }
+    
+    /**
+     * Generate entities
+     * 
+     * @param int 		$amount Number of entities to generate
+     * @return array 			Array of TSTest_TestEntity
+     */
+    protected function _generateEntities($amount = 1)
+    {
+        $returnValue = array();
+        
+        for ($i = 0; $i < $amount; $i++)
+        {
+            $entity = new TSTest_TestEntity('partition1', 'row' . ($i + 1));
+            $entity->FullName = md5(uniqid(rand(), true));
+            $entity->Age      = rand(1, 130);
+            $entity->Visible  = rand(1,2) == 1;
+            
+            $returnValue[] = $entity;
+        }
+        
+        return $returnValue;
+    }
+}
+
+/**
+ * Test Zend_Service_WindowsAzure_Storage_TableEntity class
+ */
+class TSTest_TestEntity extends Zend_Service_WindowsAzure_Storage_TableEntity
+{
+    /**
+     * @azure Name
+     */
+    public $FullName;
+    
+    /**
+     * @azure Age Edm.Int64
+     */
+    public $Age;
+    
+    /**
+     * @azure Visible Edm.Boolean
+     */
+    public $Visible = false;
+}
+
+// Call Zend_Service_WindowsAzure_TableStorageTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Service_WindowsAzure_TableStorageTest::main") {
+    Zend_Service_WindowsAzure_TableStorageTest::main();
+}

BIN
tests/Zend/Service/WindowsAzure/_files/WindowsAzure.gif