Welcome to mirror list, hosted at ThFree Co, Russian Federation.

PersistentObject.php « Common « OpenCloud « lib « php-opencloud « 3rdparty « files_external « apps - github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 0257526d709c0970eb65df04c3226ef6a03306c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
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()
        ));
    }

}