PageRenderTime 36ms CodeModel.GetById 17ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/OData Producer for PHP/library/ODataProducer/ObjectModel/ObjectModelSerializerBase.php

#
PHP | 777 lines | 382 code | 57 blank | 338 comment | 57 complexity | 36404a8e76b3b46107c79679e93f3bd0 MD5 | raw file
  1<?php
  2/** 
  3 * Base class for object model serializer.
  4 * 
  5 * PHP version 5.3
  6 * 
  7 * @category  ODataProducer
  8 * @package   ODataProducer_ObjectModel
  9 * @author    Anu T Chandy <odataphpproducer_alias@microsoft.com>
 10 * @copyright 2011 Microsoft Corp. (http://www.microsoft.com)
 11 * @license   New BSD license, (http://www.opensource.org/licenses/bsd-license.php)
 12 * @version   SVN: 1.0
 13 * @link      http://odataphpproducer.codeplex.com
 14 * 
 15 */
 16namespace ODataProducer\ObjectModel;
 17use ODataProducer\Common\ODataConstants;
 18use ODataProducer\DataService;
 19use ODataProducer\Providers\MetadataQueryProviderWrapper;
 20use ODataProducer\Providers\Metadata\ResourceSetWrapper;
 21use ODataProducer\Providers\Metadata\ResourceProperty;
 22use ODataProducer\Providers\Metadata\ResourceTypeKind;
 23use ODataProducer\Providers\Metadata\ResourceType;
 24use ODataProducer\Providers\Metadata\Type\IType;
 25use ODataProducer\UriProcessor\RequestDescription;
 26use ODataProducer\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
 27use ODataProducer\Common\InvalidOperationException;
 28use ODataProducer\Common\ODataException;
 29use ODataProducer\Common\Messages;
 30/**
 31 * Base class for object model serializer.
 32 * 
 33 * @category  ODataProducer
 34 * @package   ODataProducer_ObjectModel
 35 * @author    Anu T Chandy <odataphpproducer_alias@microsoft.com>
 36 * @copyright 2011 Microsoft Corp. (http://www.microsoft.com)
 37 * @license   New BSD license, (http://www.opensource.org/licenses/bsd-license.php)
 38 * @version   Release: 1.0
 39 * @link      http://odataphpproducer.codeplex.com
 40 */
 41class ObjectModelSerializerBase
 42{
 43    /**
 44     * Holds refernece to the data service instance.
 45     * 
 46     * @var DataService
 47     */
 48    protected $dataService;
 49
 50    /**
 51     * Request description instance describes OData request the
 52     * the client has submitted and result of the request.
 53     * 
 54     * @var RequestDescription
 55     */
 56    protected $requestDescription;
 57
 58    /**
 59     * Collection of segment names
 60     * Remark: Read 'ObjectModelSerializerNotes.txt' for more
 61     * details about segment.
 62     * 
 63     * @var array(string)
 64     */
 65    private $_segmentNames;
 66
 67    /**
 68     * Collection of segment ResourceSetWrapper instances
 69     * Remark: Read 'ObjectModelSerializerNotes.txt' for more
 70     * details about segment.
 71     * 
 72     * @var array(ResourceSetWrapper)
 73     */
 74    private $_segmentResourceSetWrappers;
 75
 76    /**
 77     * Result counts for segments
 78     * Remark: Read 'ObjectModelSerializerNotes.txt' for more
 79     * details about segment.
 80     * 
 81     * @var array(int)
 82     */
 83    private $_segmentResultCounts;
 84
 85    /**
 86     * Collection of complex type instances used for cycle detection.
 87     * 
 88     * @var array(mixed)
 89     */
 90    protected $complexTypeInstanceCollection;
 91
 92    /**
 93     * Absolute service Uri.
 94     * 
 95     * @var string
 96     */
 97    protected $absoluteServiceUri;
 98
 99    /**
100     * Absolute service Uri with slash.
101     * 
102     * @var string
103     */
104    protected $absoluteServiceUriWithSlash;
105
106    /**
107     * Constructs a new instance of ObjectModelSerializerBase.
108     * 
109     * @param DataService        &$dataService        Reference to the 
110     *                                                data service instance.
111     * @param RequestDescription &$requestDescription Type instance describing 
112     *                                                the client submitted
113     *                                                request.
114     */
115    protected function __construct(DataService &$dataService, RequestDescription &$requestDescription)
116    {
117        $this->dataService = $dataService;
118        $this->requestDescription = $requestDescription;
119        $this->absoluteServiceUri = $dataService->getHost()->getAbsoluteServiceUri()->getUrlAsString();
120        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
121        $this->_segmentNames = array();
122        $this->_segmentResourceSetWrappers = array();
123        $this->_segmentResultCounts = array();
124        $this->complexTypeInstanceCollection = array();
125    }
126
127    /**
128     * Builds the key for the given entity instance.
129     * Note: The generated key can be directly used in the uri, 
130     * this function will perform
131     * required escaping of characters, for example:
132     * Ships(ShipName='Antonio%20Moreno%20Taquer%C3%ADa',ShipID=123),
133     * Note to method caller: Don't do urlencoding on 
134     * return value of this method as it already encoded.
135     * 
136     * @param mixed        &$entityInstance Entity instance for which 
137     *                                      key value needs to be prepared.
138     * @param ResourceType &$resourceType   Resource type instance containing 
139     *                                      metadata about the instance.
140     * @param string       $containerName   Name of the entity set that 
141     *                                      the entity instance belongs to.
142     * 
143     * @return string      Key for the given resource, with values 
144     * encoded for use in a URI.
145     */
146    protected function getEntryInstanceKey(&$entityInstance, ResourceType &$resourceType, $containerName)
147    {
148        $keyProperties = $resourceType->getKeyProperties();
149        $this->assert(count($keyProperties) != 0, 'count($keyProperties) != 0');
150        $keyString = $containerName . '(';
151        $comma = null;
152        foreach ($keyProperties as $keyName => $resourceProperty) {
153            $keyType = $resourceProperty->getInstanceType();            
154            $this->assert(
155                array_search('ODataProducer\Providers\Metadata\Type\IType', class_implements($keyType)) !== false, 
156                'array_search(\'ODataProducer\Providers\Metadata\Type\IType\', class_implements($keyType)) !== false'
157            );
158
159            $keyValue = $this->getPropertyValue($entityInstance, $resourceType, $resourceProperty);
160            if (is_null($keyValue)) {
161                ODataException::createInternalServerError(Messages::badQueryNullKeysAreNotSupported($resourceType->getName(), $keyName));
162            }
163            
164            $keyValue = $keyType->convertToOData($keyValue);
165            $keyString .= $comma . $keyName.'='.$keyValue;
166            $comma = ',';
167        }
168
169        $keyString .= ')';
170        return $keyString;
171    }
172
173    /**
174     * Get the value of a given property from an instance.
175     * 
176     * @param mixed            &$object           Instance of a type which 
177     *                                            contains this property. 
178     * @param ResourceType     &$resourceType     Resource type instance 
179     *                                            containing metadata about 
180     *                                            the instance.
181     * @param ResourceProperty &$resourceProperty Resource property instance 
182     *                                            containing metadata about the 
183     *                                            property whose value 
184     *                                            to be retrieved.
185     * 
186     * @return mixed The value of the given property.
187     * 
188     * @throws ODataException If reflection exception occured 
189     * while trying to access the property.
190     */
191    protected function getPropertyValue(&$object, ResourceType &$resourceType, ResourceProperty &$resourceProperty)
192    {
193        try {
194                $reflectionProperty = new \ReflectionProperty($object, $resourceProperty->getName());
195                $propertyValue = $reflectionProperty->getValue($object);
196                return $propertyValue;                   
197        } catch (\ReflectionException $reflectionException) {
198            throw ODataException::createInternalServerError(
199                Messages::objectModelSerializerFailedToAccessProperty(
200                    $resourceProperty->getName(), 
201                    $resourceType->getName()
202                )
203            );                
204        }
205    }
206
207    /**
208     * Resource set wrapper for the resource being serialized.
209     * 
210     * @return ResourceSetWrapper
211     */
212    protected function getCurrentResourceSetWrapper()
213    {
214        $count = count($this->_segmentResourceSetWrappers);
215        if ($count == 0) {
216            return $this->requestDescription->getTargetResourceSetWrapper();
217        } else {
218            return $this->_segmentResourceSetWrappers[$count - 1];
219        }
220    }
221
222    /**
223     * Whether the current resource set is root resource set.
224     * 
225     * @return boolean true if the current resource set root container else
226     *                 false.
227     */
228    protected function isRootResourceSet()
229    {
230        return empty($this->_segmentResourceSetWrappers) 
231                || count($this->_segmentResourceSetWrappers) == 1;
232    }
233
234    /**
235     * Returns the etag for the given resource.
236     * 
237     * @param mixed        &$entryObject  Resource for which etag value 
238     *                                    needs to be returned
239     * @param ResourceType &$resourceType Resource type of the $entryObject
240     * 
241     * @return string/NULL ETag value for the given resource 
242     * (with values encoded for use in a URI)
243     * if there are etag properties, NULL if there is no etag property.
244     */
245    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType)
246    {
247        $eTag = null;
248        $comma = null;
249        foreach ($resourceType->getETagProperties() as $eTagProperty) {
250            $type = $eTagProperty->getInstanceType();
251            $this->assert(
252                !is_null($type) 
253                && array_search('ODataProducer\Providers\Metadata\Type\IType', class_implements($type)) !== false,
254                '!is_null($type) 
255                && array_search(\'ODataProducer\Providers\Metadata\Type\IType\', class_implements($type)) !== false'
256            );
257            $value = $this->getPropertyValue($entryObject, $resourceType, $eTagProperty);
258            if (is_null($value)) {
259                $eTag = $eTag . $comma. 'null';
260            } else {
261                $eTag = $eTag . $comma . $type->convertToOData($value);
262            }
263
264            $comma = ',';
265        }
266
267        if (!is_null($eTag)) {
268            // If eTag is made up of datetime or string properties then the above
269            // IType::converToOData will perform utf8 and url encode. But we don't
270            // want this for eTag value.
271            $eTag = urldecode(utf8_decode($eTag));
272            return ODataConstants::HTTP_WEAK_ETAG_PREFIX . rtrim($eTag, ',') . '"';
273        }
274
275        return null;
276    }
277
278    /**
279     * Pushes a segment for the root of the tree being written out
280     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
281     * 'Segment Stack' and this method.
282     * Note: Calls to this method should be balanced with calls to popSegment.
283     * 
284     * @return true if the segment was pushed, false otherwise.
285     */
286    protected function pushSegmentForRoot()
287    {
288        $segmentName = $this->requestDescription->getContainerName();
289        $segmentResourceSetWrapper = $this->requestDescription->getTargetResourceSetWrapper();
290        return $this->_pushSegment($segmentName, $segmentResourceSetWrapper);
291    }
292
293    /**
294     * Pushes a segment for the current navigation property being written out.
295     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
296     * 'Segment Stack' and this method.
297     * Note: Calls to this method should be balanced with calls to popSegment.
298     * 
299     * @param ResourceProperty &$resourceProperty The current navigation property
300     * being written out.
301     * 
302     * @return true if a segment was pushed, false otherwise
303     * 
304     * @throws InvalidOperationException If this function invoked with non-navigation
305     *                                   property instance.
306     */
307    protected function pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty)
308    {
309        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
310            $this->assert(!empty($this->_segmentNames), '!is_empty($this->_segmentNames');
311            $currentResourceSetWrapper = $this->getCurrentResourceSetWrapper();
312            $currentResourceType = $currentResourceSetWrapper->getResourceType();
313            $currentResourceSetWrapper = $this->dataService
314                ->getMetadataQueryProviderWrapper()
315                ->getResourceSetWrapperForNavigationProperty(
316                    $currentResourceSetWrapper, 
317                    $currentResourceType, 
318                    $resourceProperty
319                );
320
321            $this->assert(!is_null($currentResourceSetWrapper), '!null($currentResourceSetWrapper)');
322            return $this->_pushSegment($resourceProperty->getName(), $currentResourceSetWrapper);
323        } else {
324            throw new InvalidOperationException('pushSegmentForNavigationProperty should not be called with non-entity type');
325        }
326    }
327
328    /**
329     * Gets collection of projection nodes under the current node.
330     * 
331     * @return array(ProjectionNode/ExpandedProjectionNode)/NULL List of nodes 
332     * describing projections for the current segment, If this method returns 
333     * null it means no projections are to be applied and the entire resource
334     * for the current segment should be serialized, If it returns non-null 
335     * only the properties described by the returned projection segments should 
336     * be serialized.
337     */
338    protected function getProjectionNodes()
339    {
340        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
341        if (is_null($expandedProjectionNode) 
342            || $expandedProjectionNode->canSelectAllProperties()
343        ) {
344            return null;
345        }
346
347        return $expandedProjectionNode->getChildNodes();
348    }
349
350    /**
351     * Find a 'ExpandedProjectionNode' instance in the projection tree 
352     * which describes the current segment.
353     * 
354     * @return ExpandedProjectionNode/NULL
355     */
356    protected function getCurrentExpandedProjectionNode()
357    {
358        $expandedProjectionNode = $this->requestDescription->getRootProjectionNode();
359        if (is_null($expandedProjectionNode)) {
360            return null;
361        } else {
362            $depth = count($this->_segmentNames);
363            // $depth == 1 means serialization of root entry 
364            //(the resource identified by resource path) is going on, 
365            //so control won't get into the below for loop. 
366            //we will directly return the root node, 
367            //which is 'ExpandedProjectionNode'
368            // for resource identified by resource path.
369            if ($depth != 0) {
370                for ($i = 1; $i < $depth; $i++) {
371                    $expandedProjectionNode 
372                        = $expandedProjectionNode->findNode($this->_segmentNames[$i]);
373                        $this->assert(
374                            !is_null($expandedProjectionNode), 
375                            '!is_null($expandedProjectionNode)'
376                        );
377                        $this->assert(
378                            $expandedProjectionNode instanceof ExpandedProjectionNode, 
379                            '$expandedProjectionNode instanceof ExpandedProjectionNode'
380                        );
381                }
382            } 
383        }
384
385        return $expandedProjectionNode;
386    }
387
388    /**
389     * Check whether to expand a navigation property or not.
390     * 
391     * @param string $navigationPropertyName Name of naviagtion property in question.
392     * 
393     * @return boolean True if the given navigation should be 
394     * explanded otherwise false.
395     */
396    protected function shouldExpandSegment($navigationPropertyName)
397    {
398        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
399        if (is_null($expandedProjectionNode)) {
400            return false;
401        }
402
403        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
404        return !is_null($expandedProjectionNode) && ($expandedProjectionNode instanceof ExpandedProjectionNode);
405    }
406
407    /**
408     * Pushes information about the segment that is going to be serialized 
409     * to the 'Segment Stack'.
410     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
411     * 'Segment Stack' and this method.
412     * Note: Calls to this method should be balanced with calls to popSegment.
413     * 
414     * @param string             $segmentName         Name of segment to push.
415     * @param ResourceSetWrapper &$resourceSetWrapper The resource set 
416     *                                                wrapper to push.
417     * 
418     * @return true if the segment was push, false otherwise
419     */
420    private function _pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper)
421    {
422        $rootProjectionNode = $this->requestDescription->getRootProjectionNode();
423        // Even though there is no expand in the request URI, still we need to push
424        // the segment information if we need to count 
425        //the number of entities written.
426        // After serializing each entity we should check the count to see whether  
427        // we serialized more entities than configured 
428        //(page size, maxResultPerCollection).
429        // But we will not do this check since library is doing paging and never 
430        // accumulate entities more than configured.
431        //
432        // if ((!is_null($rootProjectionNode) && $rootProjectionNode->isExpansionSpecified()) 
433        //    || ($resourceSetWrapper->getResourceSetPageSize() != 0)
434        //    || ($this->dataService->getServiceConfiguration()->getMaxResultsPerCollection() != PHP_INT_MAX)            
435        //) {}
436
437        if (!is_null($rootProjectionNode) 
438            && $rootProjectionNode->isExpansionSpecified()
439        ) {
440            array_push($this->_segmentNames, $segmentName);
441            array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper);
442            array_push($this->_segmentResultCounts, 0);
443            return true;
444        }
445
446        return false;
447    }
448
449    /**
450     * Get next page link from the given entity instance.
451     * 
452     * @param mixed  &$lastObject Last object serialized to be 
453     *                            used for generating $skiptoken.
454     * @param string $absoluteUri Absolute response URI.
455     * 
456     * @return URI for the link for next page.
457     */
458    protected function getNextLinkUri(&$lastObject, $absoluteUri)
459    {
460        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
461        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
462        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
463        $this->assert(!is_null($skipToken), '!is_null($skipToken)');
464        $queryParameterString = null;
465        if ($this->isRootResourceSet()) {
466            $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
467        } else {
468            $queryParameterString = $this->getNextPageLinkQueryParametersForExpandedResourceSet();
469        }
470
471        $queryParameterString .= '$skiptoken=' . $skipToken;        
472        $odalaLink = new ODataLink();
473        $odalaLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
474        $odalaLink->url = rtrim($absoluteUri, '/') . '?' . $queryParameterString;
475        return $odalaLink;
476    }
477
478    /**
479     * Builds the string corresponding to query parameters for top level results 
480     * (result set identified by the resource path) to be put in next page link.
481     * 
482     * @return string/NULL string representing the query parameters in the URI 
483     *                     query parameter format, NULL if there 
484     *                     is no query parameters
485     *                     required for the next link of top level result set.
486     */
487    protected function getNextPageLinkQueryParametersForRootResourceSet()
488    {
489        $queryParameterString = null;
490        foreach (array(ODataConstants::HTTPQUERY_STRING_FILTER, 
491            ODataConstants::HTTPQUERY_STRING_EXPAND, 
492            ODataConstants::HTTPQUERY_STRING_ORDERBY, 
493            ODataConstants::HTTPQUERY_STRING_INLINECOUNT, 
494            ODataConstants::HTTPQUERY_STRING_SELECT) as $queryOption
495        ) {
496            $value = $this->dataService->getHost()->getQueryStringItem($queryOption);
497            if (!is_null($value)) {
498                if (!is_null($queryParameterString)) {
499                    $queryParameterString = $queryParameterString . '&';
500                }
501
502                $queryParameterString .= $queryOption . '=' . $value;
503            }            
504        }
505
506        $topCountValue = $this->requestDescription->getTopOptionCount();
507        if (!is_null($topCountValue)) {
508            $remainingCount  = $topCountValue - $this->requestDescription->getTopCount();
509            if (!is_null($queryParameterString)) {
510                $queryParameterString .= '&';
511            }
512
513            $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
514        }
515
516        if (!is_null($queryParameterString)) {
517            $queryParameterString .= '&';
518        }
519
520        return $queryParameterString;
521    }
522
523    /**
524     * Builds the string corresponding to query parameters for current expanded
525     * results to be put in next page link.
526     * 
527     * @return string/NULL string representing the $select and $expand parameters 
528     *                     in the URI query parameter format, NULL if there is no 
529     *                     query parameters ($expand and/select) required for the 
530     *                     next link of expanded result set.
531     */
532    protected function getNextPageLinkQueryParametersForExpandedResourceSet()
533    {
534        $queryParameterString = null;
535        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
536        if (!is_null($expandedProjectionNode)) {
537            $pathSegments = array();
538            $selectionPaths = null;
539            $expansionPaths = null;            
540            $foundSelections = false;
541            $foundExpansions = false;
542            $this->_buildSelectionAndExpansionPathsForNode(
543                $pathSegments, 
544                $selectionPaths, 
545                $expansionPaths, 
546                $expandedProjectionNode, 
547                $foundSelections, 
548                $foundExpansions
549            );
550
551            if ($foundSelections && $expandedProjectionNode->canSelectAllProperties()) {
552                $this->_appendSelectionOrExpandPath($selectionPaths, $pathSegments, '*');
553            }
554
555            if (!is_null($selectionPaths)) {
556                $queryParameterString = '$select=' . $selectionPaths;
557            }
558
559            if (!is_null($expansionPaths)) {
560                if (!is_null($queryParameterString)) {
561                    $queryParameterString .= '&';
562                }
563
564                $queryParameterString = '$expand=' . $expansionPaths;
565            }
566
567            if (!is_null($queryParameterString)) {
568                    $queryParameterString .= '&';
569            }
570        }
571
572        return $queryParameterString;
573    }
574
575    /**
576     * Wheter next link is needed for the current resource set (feed) 
577     * being serialized.
578     * 
579     * @param int $resultSetCount Number of entries in the current 
580     *                            resource set.
581     * 
582     * @return boolean true if the feed must have a next page link
583     */
584    protected function needNextPageLink($resultSetCount)
585    {
586        $currentResourceSet = $this->getCurrentResourceSetWrapper();
587        $recursionLevel = count($this->_segmentNames);
588        //$this->assert($recursionLevel != 0, '$recursionLevel != 0');
589        $pageSize = $currentResourceSet->getResourceSetPageSize();       
590
591        if ($recursionLevel == 1) {
592            //presence of $top option affect next link for root container
593            $topValueCount = $this->requestDescription->getTopOptionCount();
594            if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) {
595                 return false;
596            }            
597        }
598
599        return $resultSetCount == $pageSize;
600    }
601
602    /**
603     * Pops segment information from the 'Segment Stack'
604     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
605     * 'Segment Stack' and this method.
606     * Note: Calls to this method should be balanced with previous 
607     * calls to _pushSegment.
608     * 
609     * @param boolean $needPop Is a pop required. Only true if last 
610     *                         push was successful.
611     * 
612     * @return void
613     * 
614     * @throws InvalidOperationException If found un-balanced call with _pushSegment
615     */
616    protected function popSegment($needPop)
617    {
618        if ($needPop) {
619            if (!empty($this->_segmentNames)) {
620                array_pop($this->_segmentNames);
621                array_pop($this->_segmentResourceSetWrappers);
622                array_pop($this->_segmentResultCounts);
623            } else {
624                throw new InvalidOperationException('Found non-balanced call to _pushSegment and popSegment');
625            }
626        }
627    }
628
629    /**
630     * Recursive metod to build $expand and $select paths for a specified node.
631     * 
632     * @param array(string)          &$parentPathSegments     Array of path 
633     *                                                        segments which leads
634     *                                                        up to (including) 
635     *                                                        the segment 
636     *                                                        represented by 
637     *                                                        $expandedProjectionNode.
638     * @param array(string)          &$selectionPaths         The string which 
639     *                                                        holds projection
640     *                                                        path segment 
641     *                                                        seperated by comma,
642     *                                                        On return this argument
643     *                                                        will be updated with 
644     *                                                        the selection path
645     *                                                        segments under 
646     *                                                        this node. 
647     * @param array(string)          &$expansionPaths         The string which holds
648     *                                                        expansion path segment
649     *                                                        seperated by comma.
650     *                                                        On return this argument
651     *                                                        will be updated with 
652     *                                                        the expand path
653     *                                                        segments under 
654     *                                                        this node. 
655     * @param ExpandedProjectionNode &$expandedProjectionNode The expanded node for 
656     *                                                        which expansion 
657     *                                                        and selection path 
658     *                                                        to be build.
659     * @param boolean                &$foundSelections        On return, this 
660     *                                                        argument will hold
661     *                                                        true if any selection
662     *                                                        defined under this node
663     *                                                        false otherwise.
664     * @param boolean                &$foundExpansions        On return, this 
665     *                                                        argument will hold 
666     *                                                        true if any expansion
667     *                                                        defined under this node
668     *                                                        false otherwise.
669     *
670     * @return void
671     */
672    private function _buildSelectionAndExpansionPathsForNode(&$parentPathSegments, 
673        &$selectionPaths, &$expansionPaths, 
674        ExpandedProjectionNode &$expandedProjectionNode, 
675        &$foundSelections, &$foundExpansions
676    ) {
677        $foundSelections = false;
678        $foundExpansions = false;
679        $foundSelectionOnChild = false;
680        $foundExpansionOnChild = false;
681        $expandedChildrenNeededToBeSelected = array();
682        foreach ($expandedProjectionNode->getChildNodes() as $childNode) {
683            if (!($childNode instanceof ExpandedProjectionNode)) {
684                $foundSelections = true;
685                $this->_appendSelectionOrExpandPath(
686                    $selectionPaths, 
687                    $parentPathSegments, 
688                    $childNode->getPropertyName()
689                );
690            } else {
691                $foundExpansions = true;
692                array_push($parentPathSegments, $childNode->getPropertyName());
693                $this->_buildSelectionAndExpansionPathsForNode(
694                    $parentPathSegments, 
695                    $selectionPaths, $expansionPaths, 
696                    $childNode, $foundSelectionOnChild, 
697                    $foundExpansionOnChild
698                );
699                array_pop($parentPathSegments);
700                if ($childNode->canSelectAllProperties()) {
701                    if ($foundSelectionOnChild) {
702                        $this->_appendSelectionOrExpandPath(
703                            $selectionPaths, 
704                            $parentPathSegments, 
705                            $childNode->getPropertyName() . '/*'
706                        );
707                    } else {
708                        $expandedChildrenNeededToBeSelected[] = $childNode;
709                    }
710                }
711            }
712
713            $foundSelections |= $foundSelectionOnChild;
714            if (!$foundExpansionOnChild) {
715                $this->_appendSelectionOrExpandPath(
716                    $expansionPaths, 
717                    $parentPathSegments, 
718                    $childNode->getPropertyName()
719                );
720            }
721        }
722
723        if (!$expandedProjectionNode->canSelectAllProperties() || $foundSelections) {
724            foreach ($expandedChildrenNeededToBeSelected as $childToProject) {
725                $this->_appendSelectionOrExpandPath(
726                    $selectionPaths, 
727                    $parentPathSegments, 
728                    $childNode->getPropertyName()
729                );
730                $foundSelections = true;
731            }
732        }
733    }
734
735    /**
736     * Append the given path to $expand or $select path list.
737     * 
738     * @param string        &$path               The $expand or $select path list
739     *                                           to which to append the given path.
740     * @param array(string) &$parentPathSegments The list of path upto the 
741     *                                           $segmentToAppend.
742     * @param string        $segmentToAppend     The last segment of the path.
743     * 
744     * @return void
745     */
746    private function _appendSelectionOrExpandPath(&$path, &$parentPathSegments, $segmentToAppend)
747    {
748        if (!is_null($path)) {
749            $path .= ', ';
750        }
751
752        foreach ($parentPathSegments as $parentPathSegment) {
753            $path .= $parentPathSegment . '/';
754        }
755
756        $path .= $segmentToAppend;
757    }
758
759    /**
760     * Assert that the given condition is true.
761     * 
762     * @param boolean $condition         Condition to be asserted.
763     * @param string  $conditionAsString String containing message incase
764     *                                   if assertion fails.
765     * 
766     * @throws InvalidOperationException Incase if assertion failes.
767     * 
768     * @return void
769     */
770    protected function assert($condition, $conditionAsString)
771    {
772        if (!$condition) {
773            throw new InvalidOperationException("Unexpected state, expecting $conditionAsString");
774        }
775    }
776}
777?>