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'); } } }