diff options
Diffstat (limited to 'apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/PersistentObject.php')
-rw-r--r-- | apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/PersistentObject.php | 939 |
1 files changed, 939 insertions, 0 deletions
diff --git a/apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/PersistentObject.php b/apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/PersistentObject.php new file mode 100644 index 00000000000..0257526d709 --- /dev/null +++ b/apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/PersistentObject.php @@ -0,0 +1,939 @@ +<?php +/** + * An abstraction that defines persistent objects associated with a service + * + * @copyright 2012-2013 Rackspace Hosting, Inc. + * See COPYING for licensing information + * + * @package phpOpenCloud + * @version 1.0 + * @author Glen Campbell <glen.campbell@rackspace.com> + * @author Jamie Hannaford <jamie.hannaford@rackspace.com> + */ + +namespace OpenCloud\Common; + +/** + * Represents an object that can be retrieved, created, updated and deleted. + * + * This class abstracts much of the common functionality between: + * + * * Nova servers; + * * Swift containers and objects; + * * DBAAS instances; + * * Cinder volumes; + * * and various other objects that: + * * have a URL; + * * can be created, updated, deleted, or retrieved; + * * use a standard JSON format with a top-level element followed by + * a child object with attributes. + * + * In general, you can create a persistent object class by subclassing this + * class and defining some protected, static variables: + * + * * $url_resource - the sub-resource value in the URL of the parent. For + * example, if the parent URL is `http://something/parent`, then setting this + * value to "another" would result in a URL for the persistent object of + * `http://something/parent/another`. + * + * * $json_name - the top-level JSON object name. For example, if the + * persistent object is represented by `{"foo": {"attr":value, ...}}`, then + * set $json_name to "foo". + * + * * $json_collection_name - optional; this value is the name of a collection + * of the persistent objects. If not provided, it defaults to `json_name` + * with an appended "s" (e.g., if `json_name` is "foo", then + * `json_collection_name` would be "foos"). Set this value if the collection + * name doesn't follow this pattern. + * + * * $json_collection_element - the common pattern for a collection is: + * `{"collection": [{"attr":"value",...}, {"attr":"value",...}, ...]}` + * That is, each element of the array is a \stdClass object containing the + * object's attributes. In rare instances, the objects in the array + * are named, and `json_collection_element` contains the name of the + * collection objects. For example, in this JSON response: + * `{"allowedDomain":[{"allowedDomain":{"name":"foo"}}]}`, + * `json_collection_element` would be set to "allowedDomain". + * + * The PersistentObject class supports the standard CRUD methods; if these are + * not needed (i.e. not supported by the service), the subclass should redefine + * these to call the `noCreate`, `noUpdate`, or `noDelete` methods, which will + * trigger an appropriate exception. For example, if an object cannot be created: + * + * function create($params = array()) + * { + * $this->noCreate(); + * } + */ +abstract class PersistentObject extends Base +{ + + private $service; + + private $parent; + + protected $id; + + /** + * Retrieves the instance from persistent storage + * + * @param mixed $service The service object for this resource + * @param mixed $info The ID or array/object of data + */ + public function __construct($service = null, $info = null) + { + if ($service instanceof Service) { + $this->setService($service); + } + + if (property_exists($this, 'metadata')) { + $this->metadata = new Metadata; + } + + $this->populate($info); + } + + /** + * Validates properties that have a namespace: prefix + * + * If the property prefix: appears in the list of supported extension + * namespaces, then the property is applied to the object. Otherwise, + * an exception is thrown. + * + * @param string $name the name of the property + * @param mixed $value the property's value + * @return void + * @throws AttributeError + */ + public function __set($name, $value) + { + $this->setProperty($name, $value, $this->getService()->namespaces()); + } + + /** + * Sets the service associated with this resource object. + * + * @param \OpenCloud\Common\Service $service + */ + public function setService(Service $service) + { + $this->service = $service; + return $this; + } + + /** + * Returns the service object for this resource; required for making + * requests, etc. because it has direct access to the Connection. + * + * @return \OpenCloud\Common\Service + */ + public function getService() + { + if (null === $this->service) { + throw new Exceptions\ServiceValueError( + 'No service defined' + ); + } + return $this->service; + } + + /** + * Legacy shortcut to getService + * + * @return \OpenCloud\Common\Service + */ + public function service() + { + return $this->getService(); + } + + /** + * Set the parent object for this resource. + * + * @param \OpenCloud\Common\PersistentObject $parent + */ + public function setParent(PersistentObject $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * Returns the parent. + * + * @return \OpenCloud\Common\PersistentObject + */ + public function getParent() + { + if (null === $this->parent) { + $this->parent = $this->getService(); + } + return $this->parent; + } + + /** + * Legacy shortcut to getParent + * + * @return \OpenCloud\Common\PersistentObject + */ + public function parent() + { + return $this->getParent(); + } + + + + + /** + * API OPERATIONS (CRUD & CUSTOM) + */ + + /** + * Creates a new object + * + * @api + * @param array $params array of values to set when creating the object + * @return HttpResponse + * @throws VolumeCreateError if HTTP status is not Success + */ + public function create($params = array()) + { + // set parameters + if (!empty($params)) { + $this->populate($params, false); + } + + // debug + $this->getLogger()->info('{class}::Create({name})', array( + 'class' => get_class($this), + 'name' => $this->Name() + )); + + // construct the JSON + $object = $this->createJson(); + $json = json_encode($object); + $this->checkJsonError(); + + $this->getLogger()->info('{class}::Create JSON [{json}]', array( + 'class' => get_class($this), + 'json' => $json + )); + + // send the request + $response = $this->getService()->request( + $this->createUrl(), + 'POST', + array('Content-Type' => 'application/json'), + $json + ); + + // check the return code + // @codeCoverageIgnoreStart + if ($response->httpStatus() > 204) { + throw new Exceptions\CreateError(sprintf( + Lang::translate('Error creating [%s] [%s], status [%d] response [%s]'), + get_class($this), + $this->Name(), + $response->HttpStatus(), + $response->HttpBody() + )); + } + + if ($response->HttpStatus() == "201" && ($location = $response->Header('Location'))) { + // follow Location header + $this->refresh(null, $location); + } else { + // set values from response + $object = json_decode($response->httpBody()); + + if (!$this->checkJsonError()) { + $top = $this->jsonName(); + if (isset($object->$top)) { + $this->populate($object->$top); + } + } + } + // @codeCoverageIgnoreEnd + + return $response; + } + + /** + * Updates an existing object + * + * @api + * @param array $params array of values to set when updating the object + * @return HttpResponse + * @throws VolumeCreateError if HTTP status is not Success + */ + public function update($params = array()) + { + // set parameters + if (!empty($params)) { + $this->populate($params); + } + + // debug + $this->getLogger()->info('{class}::Update({name})', array( + 'class' => get_class($this), + 'name' => $this->Name() + )); + + // construct the JSON + $obj = $this->updateJson($params); + $json = json_encode($obj); + + $this->checkJsonError(); + + $this->getLogger()->info('{class}::Update JSON [{json}]', array( + 'class' => get_class($this), + 'json' => $json + )); + + // send the request + $response = $this->getService()->Request( + $this->url(), + 'PUT', + array(), + $json + ); + + // check the return code + // @codeCoverageIgnoreStart + if ($response->HttpStatus() > 204) { + throw new Exceptions\UpdateError(sprintf( + Lang::translate('Error updating [%s] with [%s], status [%d] response [%s]'), + get_class($this), + $json, + $response->HttpStatus(), + $response->HttpBody() + )); + } + // @codeCoverageIgnoreEnd + + return $response; + } + + /** + * Deletes an object + * + * @api + * @return HttpResponse + * @throws DeleteError if HTTP status is not Success + */ + public function delete() + { + $this->getLogger()->info('{class}::Delete()', array('class' => get_class($this))); + + // send the request + $response = $this->getService()->request($this->url(), 'DELETE'); + + // check the return code + // @codeCoverageIgnoreStart + if ($response->HttpStatus() > 204) { + throw new Exceptions\DeleteError(sprintf( + Lang::translate('Error deleting [%s] [%s], status [%d] response [%s]'), + get_class(), + $this->Name(), + $response->HttpStatus(), + $response->HttpBody() + )); + } + // @codeCoverageIgnoreEnd + + return $response; + } + + /** + * Returns an object for the Create() method JSON + * Must be overridden in a child class. + * + * @throws CreateError if not overridden + */ + protected function createJson() + { + throw new Exceptions\CreateError(sprintf( + Lang::translate('[%s] CreateJson() must be overridden'), + get_class($this) + )); + } + + /** + * Returns an object for the Update() method JSON + * Must be overridden in a child class. + * + * @throws UpdateError if not overridden + */ + protected function updateJson($params = array()) + { + throw new Exceptions\UpdateError(sprintf( + Lang::translate('[%s] UpdateJson() must be overridden'), + get_class($this) + )); + } + + /** + * throws a CreateError for subclasses that don't support Create + * + * @throws CreateError + */ + protected function noCreate() + { + throw new Exceptions\CreateError(sprintf( + Lang::translate('[%s] does not support Create()'), + get_class() + )); + } + + /** + * throws a DeleteError for subclasses that don't support Delete + * + * @throws DeleteError + */ + protected function noDelete() + { + throw new Exceptions\DeleteError(sprintf( + Lang::translate('[%s] does not support Delete()'), + get_class() + )); + } + + /** + * throws a UpdateError for subclasses that don't support Update + * + * @throws UpdateError + */ + protected function noUpdate() + { + throw new Exceptions\UpdateError(sprintf( + Lang::translate('[%s] does not support Update()'), + get_class() + )); + } + + /** + * Returns the default URL of the object + * + * This may have to be overridden in subclasses. + * + * @param string $subresource optional sub-resource string + * @param array $qstr optional k/v pairs for query strings + * @return string + * @throws UrlError if URL is not defined + */ + public function url($subresource = null, $queryString = array()) + { + // find the primary key attribute name + $primaryKey = $this->primaryKeyField(); + + // first, see if we have a [self] link + $url = $this->findLink('self'); + + /** + * Next, check to see if we have an ID + * Note that we use Parent() instead of Service(), since the parent + * object might not be a service. + */ + if (!$url && $this->$primaryKey) { + $url = Lang::noslash($this->getParent()->url($this->resourceName())) . '/' . $this->$primaryKey; + } + + // add the subresource + if ($url) { + $url .= $subresource ? "/$subresource" : ''; + if (count($queryString)) { + $url .= '?' . $this->makeQueryString($queryString); + } + return $url; + } + + // otherwise, we don't have a URL yet + throw new Exceptions\UrlError(sprintf( + Lang::translate('%s does not have a URL yet'), + get_class($this) + )); + } + + /** + * Waits for the server/instance status to change + * + * This function repeatedly polls the system for a change in server + * status. Once the status reaches the `$terminal` value (or 'ERROR'), + * then the function returns. + * + * The polling interval is set by the constant RAXSDK_POLL_INTERVAL. + * + * The function will automatically terminate after RAXSDK_SERVER_MAXTIMEOUT + * seconds elapse. + * + * @api + * @param string $terminal the terminal state to wait for + * @param integer $timeout the max time (in seconds) to wait + * @param callable $callback a callback function that is invoked with + * each repetition of the polling sequence. This can be used, for + * example, to update a status display or to permit other operations + * to continue + * @return void + */ + public function waitFor( + $terminal = 'ACTIVE', + $timeout = RAXSDK_SERVER_MAXTIMEOUT, + $callback = NULL, + $sleep = RAXSDK_POLL_INTERVAL + ) { + // find the primary key field + $primaryKey = $this->PrimaryKeyField(); + + // save stats + $startTime = time(); + + $states = array('ERROR', $terminal); + + while (true) { + + $this->refresh($this->$primaryKey); + + if ($callback) { + call_user_func($callback, $this); + } + + if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) { + return; + } + // @codeCoverageIgnoreStart + sleep($sleep); + } + } + // @codeCoverageIgnoreEnd + + /** + * Refreshes the object from the origin (useful when the server is + * changing states) + * + * @return void + * @throws IdRequiredError + */ + public function refresh($id = null, $url = null) + { + $primaryKey = $this->PrimaryKeyField(); + + if (!$url) { + if ($id === null) { + $id = $this->$primaryKey; + } + + if (!$id) { + throw new Exceptions\IdRequiredError(sprintf( + Lang::translate('%s has no ID; cannot be refreshed'), + get_class()) + ); + } + + // retrieve it + $this->getLogger()->info(Lang::translate('{class} id [{id}]'), array( + 'class' => get_class($this), + 'id' => $id + )); + + $this->$primaryKey = $id; + $url = $this->url(); + } + + // reset status, if available + if (property_exists($this, 'status')) { + $this->status = null; + } + + // perform a GET on the URL + $response = $this->getService()->Request($url); + + // check status codes + // @codeCoverageIgnoreStart + if ($response->HttpStatus() == 404) { + throw new Exceptions\InstanceNotFound( + sprintf(Lang::translate('%s [%s] not found [%s]'), + get_class($this), + $this->$primaryKey, + $url + )); + } + + if ($response->HttpStatus() >= 300) { + throw new Exceptions\UnknownError( + sprintf(Lang::translate('Unexpected %s error [%d] [%s]'), + get_class($this), + $response->HttpStatus(), + $response->HttpBody() + )); + } + + // check for empty response + if (!$response->HttpBody()) { + throw new Exceptions\EmptyResponseError( + sprintf(Lang::translate('%s::Refresh() unexpected empty response, URL [%s]'), + get_class($this), + $url + )); + } + + // we're ok, reload the response + if ($json = $response->HttpBody()) { + + $this->getLogger()->info('refresh() JSON [{json}]', array('json' => $json)); + + $response = json_decode($json); + + if ($this->CheckJsonError()) { + throw new Exceptions\ServerJsonError(sprintf( + Lang::translate('JSON parse error on %s refresh'), + get_class($this) + )); + } + + $top = $this->JsonName(); + + if ($top && isset($response->$top)) { + $content = $response->$top; + } else { + $content = $response; + } + + $this->populate($content); + + } + // @codeCoverageIgnoreEnd + } + + + /** + * OBJECT INFORMATION + */ + + /** + * Returns the displayable name of the object + * + * Can be overridden by child objects; *must* be overridden by child + * objects if the object does not have a `name` attribute defined. + * + * @api + * @return string + * @throws NameError if attribute 'name' is not defined + */ + public function name() + { + if (property_exists($this, 'name')) { + return $this->name; + } else { + throw new Exceptions\NameError(sprintf( + Lang::translate('Name attribute does not exist for [%s]'), + get_class($this) + )); + } + } + + /** + * Sends the json string to the /action resource + * + * This is used for many purposes, such as rebooting the server, + * setting the root password, creating images, etc. + * Since it can only be used on a live server, it checks for a valid ID. + * + * @param $object - this will be encoded as json, and we handle all the JSON + * error-checking in one place + * @throws ServerIdError if server ID is not defined + * @throws ServerActionError on other errors + * @returns boolean; TRUE if successful, FALSE otherwise + */ + protected function action($object) + { + $primaryKey = $this->primaryKeyField(); + + if (!$this->$primaryKey) { + throw new Exceptions\IdRequiredError(sprintf( + Lang::translate('%s is not defined'), + get_class($this) + )); + } + + if (!is_object($object)) { + throw new Exceptions\ServerActionError(sprintf( + Lang::translate('%s::Action() requires an object as its parameter'), + get_class($this) + )); + } + + // convert the object to json + $json = json_encode($object); + $this->getLogger()->info('JSON [{string}]', array('json' => $json)); + + $this->checkJsonError(); + + // debug - save the request + $this->getLogger()->info(Lang::translate('{class}::action [{json}]'), array( + 'class' => get_class($this), + 'json' => $json + )); + + // get the URL for the POST message + $url = $this->url('action'); + + // POST the message + $response = $this->getService()->request($url, 'POST', array(), $json); + + // @codeCoverageIgnoreStart + if (!is_object($response)) { + throw new Exceptions\HttpError(sprintf( + Lang::translate('Invalid response for %s::Action() request'), + get_class($this) + )); + } + + // check for errors + if ($response->HttpStatus() >= 300) { + throw new Exceptions\ServerActionError(sprintf( + Lang::translate('%s::Action() [%s] failed; response [%s]'), + get_class($this), + $url, + $response->HttpBody() + )); + } + // @codeCoverageIgnoreStart + + return $response; + } + + /** + * Execute a custom resource request. + * + * @param string $path + * @param string $method + * @param string|array|object $body + * @return boolean + * @throws Exceptions\InvalidArgumentError + * @throws Exceptions\HttpError + * @throws Exceptions\ServerActionError + */ + public function customAction($url, $method = 'GET', $body = null) + { + if (is_string($body) && (json_decode($body) === null)) { + throw new Exceptions\InvalidArgumentError( + 'Please provide either a well-formed JSON string, or an object ' + . 'for JSON serialization' + ); + } else { + $body = json_encode($body); + } + + // POST the message + $response = $this->service()->request($url, $method, array(), $body); + + if (!is_object($response)) { + throw new Exceptions\HttpError(sprintf( + Lang::translate('Invalid response for %s::customAction() request'), + get_class($this) + )); + } + + // check for errors + // @codeCoverageIgnoreStart + if ($response->HttpStatus() >= 300) { + throw new Exceptions\ServerActionError(sprintf( + Lang::translate('%s::customAction() [%s] failed; response [%s]'), + get_class($this), + $url, + $response->HttpBody() + )); + } + // @codeCoverageIgnoreEnd + + $object = json_decode($response->httpBody()); + + $this->checkJsonError(); + + return $object; + } + + /** + * returns the object's status or `N/A` if not available + * + * @api + * @return string + */ + public function status() + { + return (isset($this->status)) ? $this->status : 'N/A'; + } + + /** + * returns the object's identifier + * + * Can be overridden by a child class if the identifier is not in the + * `$id` property. Use of this function permits the `$id` attribute to + * be protected or private to prevent unauthorized overwriting for + * security. + * + * @api + * @return string + */ + public function id() + { + return $this->id; + } + + /** + * checks for `$alias` in extensions and throws an error if not present + * + * @throws UnsupportedExtensionError + */ + public function checkExtension($alias) + { + if (!in_array($alias, $this->getService()->namespaces())) { + throw new Exceptions\UnsupportedExtensionError(sprintf( + Lang::translate('Extension [%s] is not installed'), + $alias + )); + } + + return true; + } + + /** + * returns the region associated with the object + * + * navigates to the parent service to determine the region. + * + * @api + */ + public function region() + { + return $this->getService()->Region(); + } + + /** + * Since each server can have multiple links, this returns the desired one + * + * @param string $type - 'self' is most common; use 'bookmark' for + * the version-independent one + * @return string the URL from the links block + */ + public function findLink($type = 'self') + { + if (empty($this->links)) { + return false; + } + + foreach ($this->links as $link) { + if ($link->rel == $type) { + return $link->href; + } + } + + return false; + } + + /** + * returns the URL used for Create + * + * @return string + */ + protected function createUrl() + { + return $this->getParent()->Url($this->ResourceName()); + } + + /** + * Returns the primary key field for the object + * + * The primary key is usually 'id', but this function is provided so that + * (in rare cases where it is not 'id'), it can be overridden. + * + * @return string + */ + protected function primaryKeyField() + { + return 'id'; + } + + /** + * Returns the top-level document identifier for the returned response + * JSON document; must be overridden in child classes + * + * For example, a server document is (JSON) `{"server": ...}` and an + * Instance document is `{"instance": ...}` - this function must return + * the top level document name (either "server" or "instance", in + * these examples). + * + * @throws DocumentError if not overridden + */ + public static function jsonName() + { + if (isset(static::$json_name)) { + return static::$json_name; + } + + throw new Exceptions\DocumentError(sprintf( + Lang::translate('No JSON object defined for class [%s] in JsonName()'), + get_class() + )); + } + + /** + * returns the collection JSON element name + * + * When an object is returned in a collection, it usually has a top-level + * object that is an array holding child objects of the object types. + * This static function returns the name of the top-level element. Usually, + * that top-level element is simply the JSON name of the resource.'s'; + * however, it can be overridden by specifying the $json_collection_name + * attribute. + * + * @return string + */ + public static function jsonCollectionName() + { + if (isset(static::$json_collection_name)) { + return static::$json_collection_name; + } else { + return static::$json_name . 's'; + } + } + + /** + * returns the JSON name for each element in a collection + * + * Usually, elements in a collection are anonymous; this function, however, + * provides for an element level name: + * + * `{ "collection" : [ { "element" : ... } ] }` + * + * @return string + */ + public static function jsonCollectionElement() + { + if (isset(static::$json_collection_element)) { + return static::$json_collection_element; + } + } + + /** + * Returns the resource name for the URL of the object; must be overridden + * in child classes + * + * For example, a server is `/servers/`, a database instance is + * `/instances/`. Must be overridden in child classes. + * + * @throws UrlError + */ + public static function resourceName() + { + if (isset(static::$url_resource)) { + return static::$url_resource; + } + + throw new Exceptions\UrlError(sprintf( + Lang::translate('No URL resource defined for class [%s] in ResourceName()'), + get_class() + )); + } + +}
\ No newline at end of file |