File indexing completed on 2024-12-29 05:27:29
0001 <?php 0002 /** 0003 * LICENSE 0004 * 0005 * This source file is subject to the new BSD license that is bundled 0006 * with this package in the file LICENSE.txt. 0007 * It is also available through the world-wide-web at this URL: 0008 * http://framework.zend.com/license/new-bsd 0009 * If you did not receive a copy of the license and are unable to 0010 * obtain it through the world-wide-web, please send an email 0011 * to license@zend.com so we can send you a copy immediately. 0012 * 0013 * @category Zend 0014 * @package Zend_Cloud 0015 * @subpackage DocumentService 0016 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0017 * @license http://framework.zend.com/license/new-bsd New BSD License 0018 */ 0019 0020 // require_once 'Zend/Cloud/DocumentService/Adapter/AbstractAdapter.php'; 0021 // require_once 'Zend/Cloud/DocumentService/Adapter/WindowsAzure/Query.php'; 0022 // require_once 'Zend/Cloud/DocumentService/Exception.php'; 0023 // require_once 'Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php'; 0024 // require_once 'Zend/Service/WindowsAzure/Storage/Table.php'; 0025 0026 /** 0027 * SimpleDB adapter for document service. 0028 * 0029 * @category Zend 0030 * @package Zend_Cloud 0031 * @subpackage DocumentService 0032 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0033 * @license http://framework.zend.com/license/new-bsd New BSD License 0034 */ 0035 class Zend_Cloud_DocumentService_Adapter_WindowsAzure 0036 extends Zend_Cloud_DocumentService_Adapter_AbstractAdapter 0037 { 0038 /* 0039 * Options array keys for the Azure adapter. 0040 */ 0041 const ACCOUNT_NAME = 'storage_accountname'; 0042 const ACCOUNT_KEY = 'storage_accountkey'; 0043 const HOST = "storage_host"; 0044 const PROXY_HOST = "storage_proxy_host"; 0045 const PROXY_PORT = "storage_proxy_port"; 0046 const PROXY_CREDENTIALS = "storage_proxy_credentials"; 0047 const DEFAULT_PARTITION_KEY = "default_partition_key"; 0048 0049 const PARTITION_KEY = 'PartitionKey'; 0050 const ROW_KEY = 'RowKey'; 0051 const VERIFY_ETAG = "verify_etag"; 0052 const TIMESTAMP_KEY = "Timestamp"; 0053 0054 const DEFAULT_HOST = Zend_Service_WindowsAzure_Storage::URL_CLOUD_TABLE; 0055 const DEFAULT_QUERY_CLASS = 'Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query'; 0056 0057 /** 0058 * Azure service instance. 0059 * 0060 * @var Zend_Service_WindowsAzure_Storage_Table 0061 */ 0062 protected $_storageClient; 0063 0064 /** 0065 * Class to utilize for new query objects 0066 * 0067 * @var string 0068 */ 0069 protected $_queryClass = 'Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query'; 0070 0071 /** 0072 * Partition key to use by default when constructing document identifiers 0073 * @var string 0074 */ 0075 protected $_defaultPartitionKey; 0076 0077 /** 0078 * Constructor 0079 * 0080 * @param array $options 0081 * @return void 0082 */ 0083 public function __construct($options = array()) 0084 { 0085 if ($options instanceof Zend_Config) { 0086 $options = $options->toArray(); 0087 } 0088 0089 if (empty($options)) { 0090 $options = array(); 0091 } 0092 0093 if (!is_array($options)) { 0094 throw new Zend_Cloud_DocumentService_Exception('Invalid options provided'); 0095 } 0096 0097 if (isset($options[self::DOCUMENT_CLASS])) { 0098 $this->setDocumentClass($options[self::DOCUMENT_CLASS]); 0099 } 0100 0101 if (isset($options[self::DOCUMENTSET_CLASS])) { 0102 $this->setDocumentSetClass($options[self::DOCUMENTSET_CLASS]); 0103 } 0104 0105 if (isset($options[self::QUERY_CLASS])) { 0106 $this->setQueryClass($options[self::QUERY_CLASS]); 0107 } 0108 0109 // Build Zend_Service_WindowsAzure_Storage_Blob instance 0110 if (!isset($options[self::HOST])) { 0111 $host = self::DEFAULT_HOST; 0112 } else { 0113 $host = $options[self::HOST]; 0114 } 0115 0116 if (! isset($options[self::ACCOUNT_NAME])) { 0117 throw new Zend_Cloud_DocumentService_Exception('No Windows Azure account name provided.'); 0118 } 0119 0120 if (! isset($options[self::ACCOUNT_KEY])) { 0121 throw new Zend_Cloud_DocumentService_Exception('No Windows Azure account key provided.'); 0122 } 0123 0124 // TODO: support $usePathStyleUri and $retryPolicy 0125 try { 0126 $this->_storageClient = new Zend_Service_WindowsAzure_Storage_Table( 0127 $host, $options[self::ACCOUNT_NAME], $options[self::ACCOUNT_KEY]); 0128 // Parse other options 0129 if (! empty($options[self::PROXY_HOST])) { 0130 $proxyHost = $options[self::PROXY_HOST]; 0131 $proxyPort = isset($options[self::PROXY_PORT]) ? $options[self::PROXY_PORT] : 8080; 0132 $proxyCredentials = isset($options[self::PROXY_CREDENTIALS]) ? $options[self::PROXY_CREDENTIALS] : ''; 0133 $this->_storageClient->setProxy(true, $proxyHost, $proxyPort, $proxyCredentials); 0134 } 0135 if (isset($options[self::HTTP_ADAPTER])) { 0136 $this->_storageClient->setHttpClientChannel($options[self::HTTP_ADAPTER]); 0137 } 0138 } catch(Zend_Service_WindowsAzure_Exception $e) { 0139 throw new Zend_Cloud_DocumentService_Exception('Error on document service creation: '.$e->getMessage(), $e->getCode(), $e); 0140 } 0141 } 0142 0143 /** 0144 * Set the default partition key 0145 * 0146 * @param string $key 0147 * @return Zend_Cloud_DocumentService_Adapter_WindowsAzure 0148 */ 0149 public function setDefaultPartitionKey($key) 0150 { 0151 $this->_validateKey($key); 0152 $this->_defaultPartitionKey = $key; 0153 return $this; 0154 } 0155 0156 /** 0157 * Retrieve default partition key 0158 * 0159 * @return null|string 0160 */ 0161 public function getDefaultPartitionKey() 0162 { 0163 return $this->_defaultPartitionKey; 0164 } 0165 0166 /** 0167 * Create collection. 0168 * 0169 * @param string $name 0170 * @param array $options 0171 * @return boolean 0172 */ 0173 public function createCollection($name, $options = null) 0174 { 0175 if (!preg_match('/^[A-Za-z][A-Za-z0-9]{2,}$/', $name)) { 0176 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'); 0177 } 0178 try { 0179 $this->_storageClient->createTable($name); 0180 } catch(Zend_Service_WindowsAzure_Exception $e) { 0181 if (strpos($e->getMessage(), "table specified already exists") === false) { 0182 throw new Zend_Cloud_DocumentService_Exception('Error on collection creation: '.$e->getMessage(), $e->getCode(), $e); 0183 } 0184 } 0185 return true; 0186 } 0187 0188 /** 0189 * Delete collection. 0190 * 0191 * @param string $name 0192 * @param array $options 0193 * @return boolean 0194 */ 0195 public function deleteCollection($name, $options = null) 0196 { 0197 try { 0198 $this->_storageClient->deleteTable($name); 0199 } catch(Zend_Service_WindowsAzure_Exception $e) { 0200 if (strpos($e->getMessage(), "does not exist") === false) { 0201 throw new Zend_Cloud_DocumentService_Exception('Error on collection deletion: '.$e->getMessage(), $e->getCode(), $e); 0202 } 0203 } 0204 return true; 0205 } 0206 0207 /** 0208 * List collections. 0209 * 0210 * @param array $options 0211 * @return array 0212 */ 0213 public function listCollections($options = null) 0214 { 0215 try { 0216 $tables = $this->_storageClient->listTables(); 0217 $restables = array(); 0218 foreach ($tables as $table) { 0219 $restables[] = $table->name; 0220 } 0221 return $restables; 0222 } catch(Zend_Service_WindowsAzure_Exception $e) { 0223 throw new Zend_Cloud_DocumentService_Exception('Error on collection list: '.$e->getMessage(), $e->getCode(), $e); 0224 } 0225 0226 return $tables; 0227 } 0228 0229 /** 0230 * Create suitable document from array of fields 0231 * 0232 * @param array $document 0233 * @param null|string $collectionName Collection to which this document belongs 0234 * @return Zend_Cloud_DocumentService_Document 0235 */ 0236 protected function _getDocumentFromArray($document, $collectionName = null) 0237 { 0238 $key = null; 0239 if (!isset($document[Zend_Cloud_DocumentService_Document::KEY_FIELD])) { 0240 if (isset($document[self::ROW_KEY])) { 0241 $rowKey = $document[self::ROW_KEY]; 0242 unset($document[self::ROW_KEY]); 0243 if (isset($document[self::PARTITION_KEY])) { 0244 $key = array($document[self::PARTITION_KEY], $rowKey); 0245 unset($document[self::PARTITION_KEY]); 0246 } elseif (null !== ($partitionKey = $this->getDefaultPartitionKey())) { 0247 $key = array($partitionKey, $rowKey); 0248 } elseif (null !== $collectionName) { 0249 $key = array($collectionName, $rowKey); 0250 } 0251 } 0252 } else { 0253 $key = $document[Zend_Cloud_DocumentService_Document::KEY_FIELD]; 0254 unset($document[Zend_Cloud_DocumentService_Document::KEY_FIELD]); 0255 } 0256 0257 $documentClass = $this->getDocumentClass(); 0258 return new $documentClass($document, $key); 0259 } 0260 0261 /** 0262 * List all documents in a collection 0263 * 0264 * @param string $collectionName 0265 * @param null|array $options 0266 * @return Zend_Cloud_DocumentService_DocumentSet 0267 */ 0268 public function listDocuments($collectionName, array $options = null) 0269 { 0270 $select = $this->select()->from($collectionName); 0271 return $this->query($collectionName, $select); 0272 } 0273 0274 /** 0275 * Insert document 0276 * 0277 * @param array|Zend_Cloud_DocumentService_Document $document 0278 * @param array $options 0279 * @return boolean 0280 */ 0281 public function insertDocument($collectionName, $document, $options = null) 0282 { 0283 if (is_array($document)) { 0284 $document = $this->_getDocumentFromArray($document, $collectionName); 0285 } 0286 0287 if (!$document instanceof Zend_Cloud_DocumentService_Document) { 0288 throw new Zend_Cloud_DocumentService_Exception('Invalid document supplied'); 0289 } 0290 0291 $key = $this->_validateDocumentId($document->getId(), $collectionName); 0292 $document->setId($key); 0293 0294 $this->_validateCompositeKey($key); 0295 $this->_validateFields($document); 0296 try { 0297 0298 $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($key[0], $key[1]); 0299 $entity->setAzureValues($document->getFields(), true); 0300 $this->_storageClient->insertEntity($collectionName, $entity); 0301 } catch(Zend_Service_WindowsAzure_Exception $e) { 0302 throw new Zend_Cloud_DocumentService_Exception('Error on document insertion: '.$e->getMessage(), $e->getCode(), $e); 0303 } 0304 } 0305 0306 /** 0307 * Replace document. 0308 * 0309 * The new document replaces the existing document. 0310 * 0311 * @param Zend_Cloud_DocumentService_Document $document 0312 * @param array $options 0313 * @return boolean 0314 */ 0315 public function replaceDocument($collectionName, $document, $options = null) 0316 { 0317 if (is_array($document)) { 0318 $document = $this->_getDocumentFromArray($document, $collectionName); 0319 } 0320 0321 if (!$document instanceof Zend_Cloud_DocumentService_Document) { 0322 throw new Zend_Cloud_DocumentService_Exception('Invalid document supplied'); 0323 } 0324 0325 $key = $this->_validateDocumentId($document->getId(), $collectionName); 0326 $this->_validateFields($document); 0327 try { 0328 $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($key[0], $key[1]); 0329 $entity->setAzureValues($document->getFields(), true); 0330 if (isset($options[self::VERIFY_ETAG])) { 0331 $entity->setEtag($options[self::VERIFY_ETAG]); 0332 } 0333 0334 $this->_storageClient->updateEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); 0335 } catch(Zend_Service_WindowsAzure_Exception $e) { 0336 throw new Zend_Cloud_DocumentService_Exception('Error on document replace: '.$e->getMessage(), $e->getCode(), $e); 0337 } 0338 } 0339 0340 /** 0341 * Update document. 0342 * 0343 * The new document is merged the existing document. 0344 * 0345 * @param string $collectionName 0346 * @param mixed|Zend_Cloud_DocumentService_Document $documentId Document identifier or document contaiing updates 0347 * @param null|array|Zend_Cloud_DocumentService_Document Fields to update (or new fields)) 0348 * @param array $options 0349 * @return boolean 0350 */ 0351 public function updateDocument($collectionName, $documentId, $fieldset = null, $options = null) 0352 { 0353 if (null === $fieldset && $documentId instanceof Zend_Cloud_DocumentService_Document) { 0354 $fieldset = $documentId->getFields(); 0355 $documentId = $documentId->getId(); 0356 } elseif ($fieldset instanceof Zend_Cloud_DocumentService_Document) { 0357 if ($documentId == null) { 0358 $documentId = $fieldset->getId(); 0359 } 0360 $fieldset = $fieldset->getFields(); 0361 } 0362 0363 $this->_validateCompositeKey($documentId, $collectionName); 0364 $this->_validateFields($fieldset); 0365 try { 0366 $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($documentId[0], $documentId[1]); 0367 0368 // Ensure timestamp is set correctly 0369 if (isset($fieldset[self::TIMESTAMP_KEY])) { 0370 $entity->setTimestamp($fieldset[self::TIMESTAMP_KEY]); 0371 unset($fieldset[self::TIMESTAMP_KEY]); 0372 } 0373 0374 $entity->setAzureValues($fieldset, true); 0375 if (isset($options[self::VERIFY_ETAG])) { 0376 $entity->setEtag($options[self::VERIFY_ETAG]); 0377 } 0378 0379 $this->_storageClient->mergeEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); 0380 } catch(Zend_Service_WindowsAzure_Exception $e) { 0381 throw new Zend_Cloud_DocumentService_Exception('Error on document update: '.$e->getMessage(), $e->getCode(), $e); 0382 } 0383 } 0384 0385 /** 0386 * Delete document. 0387 * 0388 * @param mixed $document Document ID or Document object. 0389 * @param array $options 0390 * @return void 0391 */ 0392 public function deleteDocument($collectionName, $documentId, $options = null) 0393 { 0394 if ($documentId instanceof Zend_Cloud_DocumentService_Document) { 0395 $documentId = $documentId->getId(); 0396 } 0397 0398 $documentId = $this->_validateDocumentId($documentId, $collectionName); 0399 0400 try { 0401 $entity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($documentId[0], $documentId[1]); 0402 if (isset($options[self::VERIFY_ETAG])) { 0403 $entity->setEtag($options[self::VERIFY_ETAG]); 0404 } 0405 $this->_storageClient->deleteEntity($collectionName, $entity, isset($options[self::VERIFY_ETAG])); 0406 } catch(Zend_Service_WindowsAzure_Exception $e) { 0407 if (strpos($e->getMessage(), "does not exist") === false) { 0408 throw new Zend_Cloud_DocumentService_Exception('Error on document deletion: '.$e->getMessage(), $e->getCode(), $e); 0409 } 0410 } 0411 } 0412 0413 /** 0414 * Fetch single document by ID 0415 * 0416 * @param string $collectionName Collection name 0417 * @param mixed $documentId Document ID, adapter-dependent 0418 * @param array $options 0419 * @return Zend_Cloud_DocumentService_Document 0420 */ 0421 public function fetchDocument($collectionName, $documentId, $options = null) 0422 { 0423 $documentId = $this->_validateDocumentId($documentId, $collectionName); 0424 try { 0425 $entity = $this->_storageClient->retrieveEntityById($collectionName, $documentId[0], $documentId[1]); 0426 $documentClass = $this->getDocumentClass(); 0427 return new $documentClass($this->_resolveAttributes($entity), array($entity->getPartitionKey(), $entity->getRowKey())); 0428 } catch (Zend_Service_WindowsAzure_Exception $e) { 0429 if (strpos($e->getMessage(), "does not exist") !== false) { 0430 return false; 0431 } 0432 throw new Zend_Cloud_DocumentService_Exception('Error on document fetch: '.$e->getMessage(), $e->getCode(), $e); 0433 } 0434 } 0435 0436 /** 0437 * Query for documents stored in the document service. If a string is passed in 0438 * $query, the query string will be passed directly to the service. 0439 * 0440 * @param string $collectionName Collection name 0441 * @param string|Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query $query 0442 * @param array $options 0443 * @return array Zend_Cloud_DocumentService_DocumentSet 0444 */ 0445 public function query($collectionName, $query, $options = null) 0446 { 0447 try { 0448 if ($query instanceof Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query) { 0449 $entities = $this->_storageClient->retrieveEntities($query->assemble()); 0450 } else { 0451 $entities = $this->_storageClient->retrieveEntities($collectionName, $query); 0452 } 0453 0454 $documentClass = $this->getDocumentClass(); 0455 $resultSet = array(); 0456 foreach ($entities as $entity) { 0457 $resultSet[] = new $documentClass( 0458 $this->_resolveAttributes($entity), 0459 array($entity->getPartitionKey(), $entity->getRowKey()) 0460 ); 0461 } 0462 } catch(Zend_Service_WindowsAzure_Exception $e) { 0463 throw new Zend_Cloud_DocumentService_Exception('Error on document query: '.$e->getMessage(), $e->getCode(), $e); 0464 } 0465 0466 $setClass = $this->getDocumentSetClass(); 0467 return new $setClass($resultSet); 0468 } 0469 0470 /** 0471 * Create query statement 0472 * 0473 * @return Zend_Cloud_DocumentService_Query 0474 */ 0475 public function select($fields = null) 0476 { 0477 $queryClass = $this->getQueryClass(); 0478 if (!class_exists($queryClass)) { 0479 // require_once 'Zend/Loader.php'; 0480 Zend_Loader::loadClass($queryClass); 0481 } 0482 0483 $query = new $queryClass(); 0484 $defaultClass = self::DEFAULT_QUERY_CLASS; 0485 if (!$query instanceof $defaultClass) { 0486 throw new Zend_Cloud_DocumentService_Exception('Query class must extend ' . self::DEFAULT_QUERY_CLASS); 0487 } 0488 0489 $query->select($fields); 0490 return $query; 0491 } 0492 0493 /** 0494 * Get the concrete service client 0495 * 0496 * @return Zend_Service_WindowsAzure_Storage_Table 0497 */ 0498 public function getClient() 0499 { 0500 return $this->_storageClient; 0501 } 0502 0503 /** 0504 * Resolve table values to attributes 0505 * 0506 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity 0507 * @return array 0508 */ 0509 protected function _resolveAttributes(Zend_Service_WindowsAzure_Storage_TableEntity $entity) 0510 { 0511 $result = array(); 0512 foreach ($entity->getAzureValues() as $attr) { 0513 $result[$attr->Name] = $attr->Value; 0514 } 0515 return $result; 0516 } 0517 0518 0519 /** 0520 * Validate a partition or row key 0521 * 0522 * @param string $key 0523 * @return void 0524 * @throws Zend_Cloud_DocumentService_Exception 0525 */ 0526 protected function _validateKey($key) 0527 { 0528 if (preg_match('@[/#?' . preg_quote('\\') . ']@', $key)) { 0529 throw new Zend_Cloud_DocumentService_Exception('Invalid partition or row key provided; must not contain /, \\, #, or ? characters'); 0530 } 0531 } 0532 0533 /** 0534 * Validate a composite key 0535 * 0536 * @param array $key 0537 * @return throws Zend_Cloud_DocumentService_Exception 0538 */ 0539 protected function _validateCompositeKey(array $key) 0540 { 0541 if (2 != count($key)) { 0542 throw new Zend_Cloud_DocumentService_Exception('Invalid document key provided; must contain exactly two elements: a PartitionKey and a RowKey'); 0543 } 0544 foreach ($key as $k) { 0545 $this->_validateKey($k); 0546 } 0547 } 0548 0549 /** 0550 * Validate a document identifier 0551 * 0552 * If the identifier is an array containing a valid partition and row key, 0553 * returns it. If the identifier is a string: 0554 * - if a default partition key is present, it creates an identifier using 0555 * that and the provided document ID 0556 * - if a collection name is provided, it will use that for the partition key 0557 * - otherwise, it's invalid 0558 * 0559 * @param array|string $documentId 0560 * @param null|string $collectionName 0561 * @return array 0562 * @throws Zend_Cloud_DocumentService_Exception 0563 */ 0564 protected function _validateDocumentId($documentId, $collectionName = false) 0565 { 0566 if (is_array($documentId)) { 0567 $this->_validateCompositeKey($documentId); 0568 return $documentId; 0569 } 0570 if (!is_string($documentId)) { 0571 throw new Zend_Cloud_DocumentService_Exception('Invalid document identifier; must be a string or an array'); 0572 } 0573 0574 $this->_validateKey($documentId); 0575 0576 if (null !== ($partitionKey = $this->getDefaultPartitionKey())) { 0577 return array($partitionKey, $documentId); 0578 } 0579 if (null !== $collectionName) { 0580 return array($collectionName, $documentId); 0581 } 0582 throw new Zend_Cloud_DocumentService_Exception('Cannot determine partition name; invalid document identifier'); 0583 } 0584 0585 /** 0586 * Validate a document's fields for well-formedness 0587 * 0588 * Since Azure uses Atom, and fieldnames are included as part of XML 0589 * element tag names, the field names must be valid XML names. 0590 * 0591 * @param Zend_Cloud_DocumentService_Document|array $document 0592 * @return void 0593 * @throws Zend_Cloud_DocumentService_Exception 0594 */ 0595 public function _validateFields($document) 0596 { 0597 if ($document instanceof Zend_Cloud_DocumentService_Document) { 0598 $document = $document->getFields(); 0599 } elseif (!is_array($document)) { 0600 throw new Zend_Cloud_DocumentService_Exception('Cannot inspect fields; invalid type provided'); 0601 } 0602 0603 foreach (array_keys($document) as $key) { 0604 $this->_validateFieldKey($key); 0605 } 0606 } 0607 0608 /** 0609 * Validate an individual field name for well-formedness 0610 * 0611 * Since Azure uses Atom, and fieldnames are included as part of XML 0612 * element tag names, the field names must be valid XML names. 0613 * 0614 * While we could potentially normalize names, this could also lead to 0615 * conflict with other field names -- which we should avoid. As such, 0616 * invalid field names will raise an exception. 0617 * 0618 * @param string $key 0619 * @return void 0620 * @throws Zend_Cloud_DocumentService_Exception 0621 */ 0622 public function _validateFieldKey($key) 0623 { 0624 if (!preg_match('/^[_A-Za-z][-._A-Za-z0-9]*$/', $key)) { 0625 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'); 0626 } 0627 } 0628 }