diff -r 000000000000 -r 4eba9c11703f web/Zend/Cloud/DocumentService/Adapter/WindowsAzure.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/Zend/Cloud/DocumentService/Adapter/WindowsAzure.php Mon Dec 13 18:29:26 2010 +0100 @@ -0,0 +1,628 @@ +toArray(); + } + + if (empty($options)) { + $options = array(); + } + + if (!is_array($options)) { + throw new Zend_Cloud_DocumentService_Exception('Invalid options provided'); + } + + if (isset($options[self::DOCUMENT_CLASS])) { + $this->setDocumentClass($options[self::DOCUMENT_CLASS]); + } + + if (isset($options[self::DOCUMENTSET_CLASS])) { + $this->setDocumentSetClass($options[self::DOCUMENTSET_CLASS]); + } + + if (isset($options[self::QUERY_CLASS])) { + $this->setQueryClass($options[self::QUERY_CLASS]); + } + + // Build Zend_Service_WindowsAzure_Storage_Blob instance + if (!isset($options[self::HOST])) { + $host = self::DEFAULT_HOST; + } else { + $host = $options[self::HOST]; + } + + if (! isset($options[self::ACCOUNT_NAME])) { + throw new Zend_Cloud_DocumentService_Exception('No Windows Azure account name provided.'); + } + + if (! isset($options[self::ACCOUNT_KEY])) { + throw new Zend_Cloud_DocumentService_Exception('No Windows Azure account key provided.'); + } + + // TODO: support $usePathStyleUri and $retryPolicy + try { + $this->_storageClient = new Zend_Service_WindowsAzure_Storage_Table( + $host, $options[self::ACCOUNT_NAME], $options[self::ACCOUNT_KEY]); + // Parse other options + if (! empty($options[self::PROXY_HOST])) { + $proxyHost = $options[self::PROXY_HOST]; + $proxyPort = isset($options[self::PROXY_PORT]) ? $options[self::PROXY_PORT] : 8080; + $proxyCredentials = isset($options[self::PROXY_CREDENTIALS]) ? $options[self::PROXY_CREDENTIALS] : ''; + $this->_storageClient->setProxy(true, $proxyHost, $proxyPort, $proxyCredentials); + } + if (isset($options[self::HTTP_ADAPTER])) { + $this->_storageClient->setHttpClientChannel($options[self::HTTP_ADAPTER]); + } + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on document service creation: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Set the default partition key + * + * @param string $key + * @return Zend_Cloud_DocumentService_Adapter_WindowsAzure + */ + public function setDefaultPartitionKey($key) + { + $this->_validateKey($key); + $this->_defaultPartitionKey = $key; + return $this; + } + + /** + * Retrieve default partition key + * + * @return null|string + */ + public function getDefaultPartitionKey() + { + return $this->_defaultPartitionKey; + } + + /** + * Create collection. + * + * @param string $name + * @param array $options + * @return boolean + */ + public function createCollection($name, $options = null) + { + if (!preg_match('/^[A-Za-z][A-Za-z0-9]{2,}$/', $name)) { + throw new Zend_Cloud_DocumentService_Exception('Invalid collection name; Windows Azure collection names must consist of alphanumeric characters only, and be at least 3 characters long'); + } + try { + $this->_storageClient->createTable($name); + } catch(Zend_Service_WindowsAzure_Exception $e) { + if (strpos($e->getMessage(), "table specified already exists") === false) { + throw new Zend_Cloud_DocumentService_Exception('Error on collection creation: '.$e->getMessage(), $e->getCode(), $e); + } + } + return true; + } + + /** + * Delete collection. + * + * @param string $name + * @param array $options + * @return boolean + */ + public function deleteCollection($name, $options = null) + { + try { + $this->_storageClient->deleteTable($name); + } catch(Zend_Service_WindowsAzure_Exception $e) { + if (strpos($e->getMessage(), "does not exist") === false) { + throw new Zend_Cloud_DocumentService_Exception('Error on collection deletion: '.$e->getMessage(), $e->getCode(), $e); + } + } + return true; + } + + /** + * List collections. + * + * @param array $options + * @return array + */ + public function listCollections($options = null) + { + try { + $tables = $this->_storageClient->listTables(); + $restables = array(); + foreach ($tables as $table) { + $restables[] = $table->name; + } + return $restables; + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on collection list: '.$e->getMessage(), $e->getCode(), $e); + } + + return $tables; + } + + /** + * Create suitable document from array of fields + * + * @param array $document + * @param null|string $collectionName Collection to which this document belongs + * @return Zend_Cloud_DocumentService_Document + */ + protected function _getDocumentFromArray($document, $collectionName = null) + { + $key = null; + if (!isset($document[Zend_Cloud_DocumentService_Document::KEY_FIELD])) { + if (isset($document[self::ROW_KEY])) { + $rowKey = $document[self::ROW_KEY]; + unset($document[self::ROW_KEY]); + if (isset($document[self::PARTITION_KEY])) { + $key = array($document[self::PARTITION_KEY], $rowKey); + unset($document[self::PARTITION_KEY]); + } elseif (null !== ($partitionKey = $this->getDefaultPartitionKey())) { + $key = array($partitionKey, $rowKey); + } elseif (null !== $collectionName) { + $key = array($collectionName, $rowKey); + } + } + } else { + $key = $document[Zend_Cloud_DocumentService_Document::KEY_FIELD]; + unset($document[Zend_Cloud_DocumentService_Document::KEY_FIELD]); + } + + $documentClass = $this->getDocumentClass(); + return new $documentClass($document, $key); + } + + /** + * List all documents in a collection + * + * @param string $collectionName + * @param null|array $options + * @return Zend_Cloud_DocumentService_DocumentSet + */ + public function listDocuments($collectionName, array $options = null) + { + $select = $this->select()->from($collectionName); + return $this->query($collectionName, $select); + } + + /** + * Insert document + * + * @param array|Zend_Cloud_DocumentService_Document $document + * @param array $options + * @return boolean + */ + public function insertDocument($collectionName, $document, $options = null) + { + if (is_array($document)) { + $document = $this->_getDocumentFromArray($document, $collectionName); + } + + if (!$document instanceof Zend_Cloud_DocumentService_Document) { + throw new Zend_Cloud_DocumentService_Exception('Invalid document supplied'); + } + + $key = $this->_validateDocumentId($document->getId(), $collectionName); + $document->setId($key); + + $this->_validateCompositeKey($key); + $this->_validateFields($document); + try { + + $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($key[0], $key[1]); + $entity->setAzureValues($document->getFields(), true); + $this->_storageClient->insertEntity($collectionName, $entity); + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on document insertion: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Replace document. + * + * The new document replaces the existing document. + * + * @param Zend_Cloud_DocumentService_Document $document + * @param array $options + * @return boolean + */ + public function replaceDocument($collectionName, $document, $options = null) + { + if (is_array($document)) { + $document = $this->_getDocumentFromArray($document, $collectionName); + } + + if (!$document instanceof Zend_Cloud_DocumentService_Document) { + throw new Zend_Cloud_DocumentService_Exception('Invalid document supplied'); + } + + $key = $this->_validateDocumentId($document->getId(), $collectionName); + $this->_validateFields($document); + try { + $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($key[0], $key[1]); + $entity->setAzureValues($document->getFields(), true); + if (isset($options[self::VERIFY_ETAG])) { + $entity->setEtag($options[self::VERIFY_ETAG]); + } + + $this->_storageClient->updateEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on document replace: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Update document. + * + * The new document is merged the existing document. + * + * @param string $collectionName + * @param mixed|Zend_Cloud_DocumentService_Document $documentId Document identifier or document contaiing updates + * @param null|array|Zend_Cloud_DocumentService_Document Fields to update (or new fields)) + * @param array $options + * @return boolean + */ + public function updateDocument($collectionName, $documentId, $fieldset = null, $options = null) + { + if (null === $fieldset && $documentId instanceof Zend_Cloud_DocumentService_Document) { + $fieldset = $documentId->getFields(); + $documentId = $documentId->getId(); + } elseif ($fieldset instanceof Zend_Cloud_DocumentService_Document) { + if ($documentId == null) { + $documentId = $fieldset->getId(); + } + $fieldset = $fieldset->getFields(); + } + + $this->_validateCompositeKey($documentId, $collectionName); + $this->_validateFields($fieldset); + try { + $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($documentId[0], $documentId[1]); + + // Ensure timestamp is set correctly + if (isset($fieldset[self::TIMESTAMP_KEY])) { + $entity->setTimestamp($fieldset[self::TIMESTAMP_KEY]); + unset($fieldset[self::TIMESTAMP_KEY]); + } + + $entity->setAzureValues($fieldset, true); + if (isset($options[self::VERIFY_ETAG])) { + $entity->setEtag($options[self::VERIFY_ETAG]); + } + + $this->_storageClient->mergeEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on document update: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Delete document. + * + * @param mixed $document Document ID or Document object. + * @param array $options + * @return void + */ + public function deleteDocument($collectionName, $documentId, $options = null) + { + if ($documentId instanceof Zend_Cloud_DocumentService_Document) { + $documentId = $documentId->getId(); + } + + $documentId = $this->_validateDocumentId($documentId, $collectionName); + + try { + $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($documentId[0], $documentId[1]); + if (isset($options[self::VERIFY_ETAG])) { + $entity->setEtag($options[self::VERIFY_ETAG]); + } + $this->_storageClient->deleteEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); + } catch(Zend_Service_WindowsAzure_Exception $e) { + if (strpos($e->getMessage(), "does not exist") === false) { + throw new Zend_Cloud_DocumentService_Exception('Error on document deletion: '.$e->getMessage(), $e->getCode(), $e); + } + } + } + + /** + * Fetch single document by ID + * + * @param string $collectionName Collection name + * @param mixed $documentId Document ID, adapter-dependent + * @param array $options + * @return Zend_Cloud_DocumentService_Document + */ + public function fetchDocument($collectionName, $documentId, $options = null) + { + $documentId = $this->_validateDocumentId($documentId, $collectionName); + try { + $entity = $this->_storageClient->retrieveEntityById($collectionName, $documentId[0], $documentId[1]); + $documentClass = $this->getDocumentClass(); + return new $documentClass($this->_resolveAttributes($entity), array($entity->getPartitionKey(), $entity->getRowKey())); + } catch (Zend_Service_WindowsAzure_Exception $e) { + if (strpos($e->getMessage(), "does not exist") !== false) { + return false; + } + throw new Zend_Cloud_DocumentService_Exception('Error on document fetch: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Query for documents stored in the document service. If a string is passed in + * $query, the query string will be passed directly to the service. + * + * @param string $collectionName Collection name + * @param string|Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query $query + * @param array $options + * @return array Zend_Cloud_DocumentService_DocumentSet + */ + public function query($collectionName, $query, $options = null) + { + try { + if ($query instanceof Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query) { + $entities = $this->_storageClient->retrieveEntities($query->assemble()); + } else { + $entities = $this->_storageClient->retrieveEntities($collectionName, $query); + } + + $documentClass = $this->getDocumentClass(); + $resultSet = array(); + foreach ($entities as $entity) { + $resultSet[] = new $documentClass( + $this->_resolveAttributes($entity), + array($entity->getPartitionKey(), $entity->getRowKey()) + ); + } + } catch(Zend_Service_WindowsAzure_Exception $e) { + throw new Zend_Cloud_DocumentService_Exception('Error on document query: '.$e->getMessage(), $e->getCode(), $e); + } + + $setClass = $this->getDocumentSetClass(); + return new $setClass($resultSet); + } + + /** + * Create query statement + * + * @return Zend_Cloud_DocumentService_Query + */ + public function select($fields = null) + { + $queryClass = $this->getQueryClass(); + if (!class_exists($queryClass)) { + require_once 'Zend/Loader.php'; + Zend_Loader::loadClass($queryClass); + } + + $query = new $queryClass(); + $defaultClass = self::DEFAULT_QUERY_CLASS; + if (!$query instanceof $defaultClass) { + throw new Zend_Cloud_DocumentService_Exception('Query class must extend ' . self::DEFAULT_QUERY_CLASS); + } + + $query->select($fields); + return $query; + } + + /** + * Get the concrete service client + * + * @return Zend_Service_WindowsAzure_Storage_Table + */ + public function getClient() + { + return $this->_storageClient; + } + + /** + * Resolve table values to attributes + * + * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity + * @return array + */ + protected function _resolveAttributes(Zend_Service_WindowsAzure_Storage_TableEntity $entity) + { + $result = array(); + foreach ($entity->getAzureValues() as $attr) { + $result[$attr->Name] = $attr->Value; + } + return $result; + } + + + /** + * Validate a partition or row key + * + * @param string $key + * @return void + * @throws Zend_Cloud_DocumentService_Exception + */ + protected function _validateKey($key) + { + if (preg_match('@[/#?' . preg_quote('\\') . ']@', $key)) { + throw new Zend_Cloud_DocumentService_Exception('Invalid partition or row key provided; must not contain /, \\, #, or ? characters'); + } + } + + /** + * Validate a composite key + * + * @param array $key + * @return throws Zend_Cloud_DocumentService_Exception + */ + protected function _validateCompositeKey(array $key) + { + if (2 != count($key)) { + throw new Zend_Cloud_DocumentService_Exception('Invalid document key provided; must contain exactly two elements: a PartitionKey and a RowKey'); + } + foreach ($key as $k) { + $this->_validateKey($k); + } + } + + /** + * Validate a document identifier + * + * If the identifier is an array containing a valid partition and row key, + * returns it. If the identifier is a string: + * - if a default partition key is present, it creates an identifier using + * that and the provided document ID + * - if a collection name is provided, it will use that for the partition key + * - otherwise, it's invalid + * + * @param array|string $documentId + * @param null|string $collectionName + * @return array + * @throws Zend_Cloud_DocumentService_Exception + */ + protected function _validateDocumentId($documentId, $collectionName = false) + { + if (is_array($documentId)) { + $this->_validateCompositeKey($documentId); + return $documentId; + } + if (!is_string($documentId)) { + throw new Zend_Cloud_DocumentService_Exception('Invalid document identifier; must be a string or an array'); + } + + $this->_validateKey($documentId); + + if (null !== ($partitionKey = $this->getDefaultPartitionKey())) { + return array($partitionKey, $documentId); + } + if (null !== $collectionName) { + return array($collectionName, $documentId); + } + throw new Zend_Cloud_DocumentService_Exception('Cannot determine partition name; invalid document identifier'); + } + + /** + * Validate a document's fields for well-formedness + * + * Since Azure uses Atom, and fieldnames are included as part of XML + * element tag names, the field names must be valid XML names. + * + * @param Zend_Cloud_DocumentService_Document|array $document + * @return void + * @throws Zend_Cloud_DocumentService_Exception + */ + public function _validateFields($document) + { + if ($document instanceof Zend_Cloud_DocumentService_Document) { + $document = $document->getFields(); + } elseif (!is_array($document)) { + throw new Zend_Cloud_DocumentService_Exception('Cannot inspect fields; invalid type provided'); + } + + foreach (array_keys($document) as $key) { + $this->_validateFieldKey($key); + } + } + + /** + * Validate an individual field name for well-formedness + * + * Since Azure uses Atom, and fieldnames are included as part of XML + * element tag names, the field names must be valid XML names. + * + * While we could potentially normalize names, this could also lead to + * conflict with other field names -- which we should avoid. As such, + * invalid field names will raise an exception. + * + * @param string $key + * @return void + * @throws Zend_Cloud_DocumentService_Exception + */ + public function _validateFieldKey($key) + { + if (!preg_match('/^[_A-Za-z][-._A-Za-z0-9]*$/', $key)) { + throw new Zend_Cloud_DocumentService_Exception('Field keys must conform to XML names (^[_A-Za-z][-._A-Za-z0-9]*$); key "' . $key . '" does not match'); + } + } +}