      1: <?php
      2: /**
      3:  * Copyright 2012-2014 Rackspace US, Inc.
      4:  *
      5:  * Licensed under the Apache License, Version 2.0 (the "License");
      6:  * you may not use this file except in compliance with the License.
      7:  * You may obtain a copy of the License at
      8:  *
      9:  * http://www.apache.org/licenses/LICENSE-2.0
     10:  *
     11:  * Unless required by applicable law or agreed to in writing, software
     12:  * distributed under the License is distributed on an "AS IS" BASIS,
     13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14:  * See the License for the specific language governing permissions and
     15:  * limitations under the License.
     16:  */
     17:
     18: namespace OpenCloud\Common\Resource;
     19:
     20: use Guzzle\Http\Url;
     21: use OpenCloud\Common\Constants\State;
     22: use OpenCloud\Common\Exceptions\CreateError;
     23: use OpenCloud\Common\Exceptions\DeleteError;
     24: use OpenCloud\Common\Exceptions\IdRequiredError;
     25: use OpenCloud\Common\Exceptions\NameError;
     26: use OpenCloud\Common\Exceptions\UnsupportedExtensionError;
     27: use OpenCloud\Common\Exceptions\UpdateError;
     28:
     29: abstract class PersistentResource extends BaseResource
     30: {
     31:     /**
     32:      * Create a new resource
     33:      *
     34:      * @param array $params
     35:      * @return \Guzzle\Http\Message\Response
     36:      */
     37:     public function create($params = array())
     38:     {
     39:         // set parameters
     40:         if (!empty($params)) {
     41:             $this->populate($params, false);
     42:         }
     43:
     44:         // construct the JSON
     45:         $json = json_encode($this->createJson());
     46:         $this->checkJsonError();
     47:
     48:         $createUrl = $this->createUrl();
     49:
     50:         $response = $this->getClient()->post($createUrl, self::getJsonHeader(), $json)->send();
     51:
     52:         // We have to try to parse the response body first because it should have precedence over a Location refresh.
     53:         // I'd like to reverse the order, but Nova instances return ephemeral properties on creation which are not
     54:         // available when you follow the Location link...
     55:         if (null !== ($decoded = $this->parseResponse($response))) {
     56:             $this->populate($decoded);
     57:         } elseif ($location = $response->getHeader('Location')) {
     58:             $this->refreshFromLocationUrl($location);
     59:         }
     60:
     61:         return $response;
     62:     }
     63:
     64:     /**
     65:      * Update a resource
     66:      *
     67:      * @param array $params
     68:      * @return \Guzzle\Http\Message\Response
     69:      */
     70:     public function update($params = array())
     71:     {
     72:         // set parameters
     73:         if (!empty($params)) {
     74:             $this->populate($params);
     75:         }
     76:
     77:         // construct the JSON
     78:         $json = json_encode($this->updateJson($params));
     79:         $this->checkJsonError();
     80:
     81:         // send the request
     82:         return $this->getClient()->put($this->getUrl(), self::getJsonHeader(), $json)->send();
     83:     }
     84:
     85:     /**
     86:      * Delete this resource
     87:      *
     88:      * @return \Guzzle\Http\Message\Response
     89:      */
     90:     public function delete()
     91:     {
     92:         return $this->getClient()->delete($this->getUrl())->send();
     93:     }
     94:
     95:     /**
     96:      * Refresh the state of a resource
     97:      *
     98:      * @param null $id
     99:      * @param null $url
    100:      * @return \Guzzle\Http\Message\Response
    101:      * @throws IdRequiredError
    102:      */
    103:     public function refresh($id = null, $url = null)
    104:     {
    105:         $primaryKey = $this->primaryKeyField();
    106:         $primaryKeyVal = $this->getProperty($primaryKey);
    107:
    108:         if (!$url) {
    109:             if (!$id = $id ?: $primaryKeyVal) {
    110:                 $message = sprintf("This resource cannot be refreshed because it has no %s", $primaryKey);
    111:                 throw new IdRequiredError($message);
    112:             }
    113:
    114:             if ($primaryKeyVal != $id) {
    115:                 $this->setProperty($primaryKey, $id);
    116:             }
    117:
    118:             $url = $this->getUrl();
    119:         }
    120:
    121:         // reset status, if available
    122:         if ($this->getProperty('status')) {
    123:             $this->setProperty('status', null);
    124:         }
    125:
    126:         $response = $this->getClient()->get($url)->send();
    127:
    128:         if (null !== ($decoded = $this->parseResponse($response))) {
    129:             $this->populate($decoded);
    130:         }
    131:
    132:         return $response;
    133:     }
    134:
    135:     /**
    136:      * Given a `location` URL, refresh this resource
    137:      *
    138:      * @param $url
    139:      */
    140:     public function refreshFromLocationUrl($url)
    141:     {
    142:         $fullUrl = Url::factory($url);
    143:
    144:         $response = $this->getClient()->get($fullUrl)->send();
    145:
    146:         if (null !== ($decoded = $this->parseResponse($response))) {
    147:             $this->populate($decoded);
    148:         }
    149:     }
    150:
    151:     /**
    152:      * A method to repeatedly poll the API resource, waiting for an eventual state change
    153:      *
    154:      * @param null $state    The expected state of the resource
    155:      * @param null $timeout  The maximum timeout to wait
    156:      * @param null $callback The callback to use to check the state
    157:      * @param null $interval How long between each refresh request
    158:      */
    159:     public function waitFor($state = null, $timeout = null, $callback = null, $interval = null)
    160:     {
    161:         $state    = $state ?: State::ACTIVE;
    162:         $timeout  = $timeout ?: State::DEFAULT_TIMEOUT;
    163:         $interval = $interval ?: State::DEFAULT_INTERVAL;
    164:
    165:         // save stats
    166:         $startTime = time();
    167:
    168:         $states = array('ERROR', $state);
    169:
    170:         while (true) {
    171:
    172:             $this->refresh($this->getProperty($this->primaryKeyField()));
    173:
    174:             if ($callback) {
    175:                 call_user_func($callback, $this);
    176:             }
    177:
    178:             if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) {
    179:                 return;
    180:             }
    181:
    182:             sleep($interval);
    183:         }
    184:     }
    185:
    186:     /**
    187:      * Provides JSON for create request body
    188:      *
    189:      * @return object
    190:      * @throws \RuntimeException
    191:      */
    192:     protected function createJson()
    193:     {
    194:         if (!isset($this->createKeys)) {
    195:             throw new \RuntimeException(sprintf(
    196:                 'This resource object [%s] must have a visible createKeys array',
    197:                 get_class($this)
    198:             ));
    199:         }
    200:
    201:         $element = (object) array();
    202:
    203:         foreach ($this->createKeys as $key) {
    204:             if (null !== ($property = $this->getProperty($key))) {
    205:                 $element->{$this->getAlias($key)} = $property;
    206:             }
    207:         }
    208:
    209:         if (isset($this->metadata) && count($this->metadata)) {
    210:             $element->metadata = (object) $this->metadata->toArray();
    211:         }
    212:
    213:         return (object) array($this->jsonName() => (object) $element);
    214:     }
    215:
    216:     /**
    217:      * Returns the alias configured for the given key. If no alias exists
    218:      * it returns the original key.
    219:      *
    220:      * @param  string $key
    221:      * @return string
    222:      */
    223:     protected function getAlias($key)
    224:     {
    225:         if (false !== ($alias = array_search($key, $this->aliases))) {
    226:             return $alias;
    227:         }
    228:
    229:         return $key;
    230:     }
    231:
    232:     /**
    233:      * Provides JSON for update request body
    234:      */
    235:     protected function updateJson($params = array())
    236:     {
    237:         throw new UpdateError('updateJson() must be overriden in order for an update to happen');
    238:     }
    239:
    240:     /**
    241:      * @throws CreateError
    242:      */
    243:     protected function noCreate()
    244:     {
    245:         throw new CreateError('This resource does not support the create operation');
    246:     }
    247:
    248:     /**
    249:      * @throws DeleteError
    250:      */
    251:     protected function noDelete()
    252:     {
    253:         throw new DeleteError('This resource does not support the delete operation');
    254:     }
    255:
    256:     /**
    257:      * @throws UpdateError
    258:      */
    259:     protected function noUpdate()
    260:     {
    261:         throw new UpdateError('his resource does not support the update operation');
    262:     }
    263:
    264:     /**
    265:      * Check whether an extension is valid
    266:      *
    267:      * @param mixed $alias The extension name
    268:      * @return bool
    269:      * @throws UnsupportedExtensionError
    270:      */
    271:     public function checkExtension($alias)
    272:     {
    273:         if (!in_array($alias, $this->getService()->namespaces())) {
    274:             throw new UnsupportedExtensionError(sprintf("%s extension is not installed", $alias));
    275:         }
    276:
    277:         return true;
    278:     }
    279:
    280:     /********  DEPRECATED METHODS ********/
    281:
    282:     /**
    283:      * @deprecated
    284:      * @return string
    285:      * @throws NameError
    286:      */
    287:     public function name()
    288:     {
    289:         if (null !== ($name = $this->getProperty('name'))) {
    290:             return $name;
    291:         } else {
    292:             throw new NameError('Name attribute does not exist for this resource');
    293:         }
    294:     }
    295:
    296:     /**
    297:      * @deprecated
    298:      * @return mixed
    299:      */
    300:     public function id()
    301:     {
    302:         return $this->id;
    303:     }
    304:
    305:     /**
    306:      * @deprecated
    307:      * @return string
    308:      */
    309:     public function status()
    310:     {
    311:         return (isset($this->status)) ? $this->status : 'N/A';
    312:     }
    313:
    314:     /**
    315:      * @deprecated
    316:      * @return mixed
    317:      */
    318:     public function region()
    319:     {
    320:         return $this->getService()->region();
    321:     }
    322:
    323:     /**
    324:      * @deprecated
    325:      * @return \Guzzle\Http\Url
    326:      */
    327:     public function createUrl()
    328:     {
    329:         return $this->getParent()->getUrl($this->resourceName());
    330:     }
    331: }
    332: